handler

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 9 Imported by: 1

Documentation

Overview

Package handler holds the vocabulary every Celerity handler is written against: the events a handler receives and the results it returns.

The types here are deliberately free of any transport. The same Request reaches a handler whether it arrived over the runtime's IPC stream or was mapped from an API Gateway event by a serverless adapter, which is what lets one handler serve both.

Index

Constants

View Source
const (
	// CodeInvalid is the fallback where nothing more specific is known.
	CodeInvalid = "invalid"
	// CodeInvalidType is a value of the wrong type, such as a string where a
	// number was expected.
	CodeInvalidType = "invalid_type"
	// CodeMalformed is an input that could not be read at all, such as a body
	// that is not valid JSON.
	CodeMalformed = "malformed"
)

Codes carried by issues this SDK raises itself. A validator is free to use its own, and anything it reports is passed through.

Variables

View Source
var (
	// ErrRouteRequired is returned when framing a binary message with no route.
	ErrRouteRequired = errors.New("celerity: a binary message must name the route it is for")
	// ErrRouteTooLong is returned when a route exceeds the single byte the
	// format gives its length.
	ErrRouteTooLong = errors.New("celerity: a binary message route must be at most 255 bytes")
	// ErrMessageIDTooLong is returned when a message id exceeds the single byte
	// the format gives its length.
	ErrMessageIDTooLong = errors.New("celerity: a binary message id must be at most 255 bytes")
	// ErrRouteReserved is returned when a route begins with a byte the protocol
	// keeps for its own messages.
	ErrRouteReserved = errors.New(
		"celerity: a binary message route must not begin with a reserved byte",
	)
	// ErrAckWithoutID is returned when a message asks to be acknowledged without
	// an id for the acknowledgement to name.
	ErrAckWithoutID = errors.New(
		"celerity: a binary message asking to be acknowledged needs an id",
	)
)

Functions

func CatchAllParam

func CatchAllParam(route string) (string, bool)

CatchAllParam returns the name of the catch-all parameter in a route template, in either form, and whether there is one.

A route has at most one: it matches the rest of the path, so nothing can follow it.

func FrameBinaryMessage

func FrameBinaryMessage(route, messageID string, requireAck bool, message []byte) ([]byte, error)

FrameBinaryMessage encodes a message in the Celerity Binary Message Format.

BinarySender.SendBinary calls this, and is how an application sends binary; this is exported for a caller composing a frame for a transport of its own.

Unlike the Node and Python SDKs, which hand framing to the runtime through their native bindings, this is the Go implementation of the format: the Go SDK reaches the runtime over IPC and has no in-process binding to call. It is therefore a second implementation of a published wire format, and is held to the same bytes by the shared conformance vectors in testdata.

Every field that cannot be represented is refused rather than truncated into a frame that would be read as something other than what was meant, which is what the runtime does with the same message.

func NormaliseRoute

func NormaliseRoute(route string) string

NormaliseRoute converts a route template from the blueprint's form into the router's form, which is what Request.Route and handler tags are built from.

/files/{path+}  ->  /files/{*path}

Types

type Ack

type Ack struct {
	Success bool
	Error   string
}

Ack acknowledges an event that produces no value of its own.

type BatchResult

type BatchResult struct {
	Failures []RecordFailure
}

BatchResult reports which records in a batch failed.

Failures are per record so that a batch where one record fails redrives only that record. The runtime leaves the named records on their source and acknowledges the rest, so this decides what is delivered again rather than only reporting what happened.

Naming nothing while returning an error answers for the whole batch, and none of it is acknowledged.

func (*BatchResult) Fail

func (r *BatchResult) Fail(messageID string, err error)

Fail records a failure for one message, to be retried or redriven.

The id must be the ConsumerRecord.MessageID the record arrived with. A name matching no record in the batch settles nothing, so the runtime refuses the whole answer rather than acknowledging a batch it cannot apply.

func (*BatchResult) Failed

func (r *BatchResult) Failed() bool

Failed reports whether any record in the batch failed.

type BinarySender

type BinarySender interface {
	// SendBinary frames every message and blocks until the outcome is known.
	//
	// A message whose parts cannot be represented in the format is refused
	// rather than truncated into a frame that would be read as something other
	// than what was meant, and no message in the call is sent.
	//
	// A partial failure returns a [SendError], as [WebSocketSender.Send] does.
	SendBinary(ctx context.Context, messages ...OutboundBinaryMessage) error
}

BinarySender sends binary messages, and is implemented only by senders whose transport carries binary frames.

Deliberately separate from WebSocketSender. API Gateway WebSocket APIs carry text frames only and reject a client that sends a binary frame by disconnecting it, so binary is not something every deploy target can do. A field on OutboundMessage would compile everywhere and work in one place; this way an application asks whether the capability is there:

if bs, ok := sender.(handler.BinarySender); ok {
	err := bs.SendBinary(ctx, msg)
}

An application built for the Celerity runtime can assert this once at startup rather than at each send.

type ConsumerBatch

type ConsumerBatch struct {
	Records    []ConsumerRecord
	SourceID   string
	SourceType string
	// Vendor is provider-specific and opaque.
	Vendor []byte
}

ConsumerBatch is a batch of messages from a queue, stream or event source.

type ConsumerRecord

type ConsumerRecord struct {
	MessageID  string
	Body       []byte
	Source     string
	EventType  string
	Attributes []byte
	Vendor     []byte
}

ConsumerRecord is one message within a ConsumerBatch.

type CustomInvoke

type CustomInvoke struct {
	HandlerName string
	Input       []byte
}

CustomInvoke is a direct invocation of a named handler.

type CustomInvokeResult

type CustomInvokeResult struct {
	Output []byte
}

CustomInvokeResult is the answer to a CustomInvoke.

Output is opaque and carried to the caller exactly as given. Whether it has to be text is that caller's constraint: the runtime's local invoke endpoint answers in JSON, so it refuses output that is not valid UTF-8.

type DetailCarrier

type DetailCarrier interface {
	error
	Details() any
}

DetailCarrier is implemented by an error that has structured detail to answer with alongside its message.

The detail is serialised into the answer as-is, so it must carry nothing a caller cannot act on and nothing describing how this is implemented.

type Error

type Error struct {
	Message string
	Type    string
	Stack   string
}

Error describes an unhandled error in user code.

func (*Error) Error

func (e *Error) Error() string

type Event

type Event struct {
	// ID identifies this dispatch and is what a cancellation names.
	ID   string
	Tag  string
	Kind Kind
	// Timestamp is when the runtime received the originating event.
	Timestamp time.Time
	// Deadline is when the runtime stops waiting. It is applied to the handler's
	// context, and the runtime enforces it independently.
	Deadline time.Time
	// TraceContext carries W3C traceparent and tracestate.
	TraceContext map[string]string

	HTTP      *Request
	WebSocket *WebSocketMessage
	Consumer  *ConsumerBatch
	Schedule  *ScheduleTrigger
	Custom    *CustomInvoke
}

Event is one dispatch, carrying exactly one source.

Handlers registered through the typed API never see this; it is the shape the layer pipeline and the adapters work in, where the source is not known statically.

type Func

type Func func(ctx context.Context, ev *Event) (*Result, error)

Func is the untyped handler shape. Registration through the typed API wraps a generic function into one of these.

type IssueReporter

type IssueReporter interface {
	error
	ValidationIssues() []ValidationIssue
}

IssueReporter is implemented by an error that can describe itself as validation issues.

It is the seam a validation library is adapted through, and the reason core doesn't depend on one. A library's own error type is wrapped in something that implements this, and its issues are answered verbatim. It mirrors the one method Schema interface the Node SDK validates through.

type Kind

type Kind string

Kind identifies the source an event came from.

const (
	KindHTTP      Kind = "http"
	KindWebSocket Kind = "websocket"
	KindConsumer  Kind = "consumer"
	KindSchedule  Kind = "schedule"
	KindCustom    Kind = "custom"
)

type OutboundBinaryMessage

type OutboundBinaryMessage struct {
	ConnectionID string
	// Route is what the message is delivered by. Required, at most 255 bytes,
	// and it may not begin with a byte the protocol reserves.
	Route string
	// FrameMessageID is the id an acknowledgement names, what deduplication
	// keys on and what a loss notification refers to. Required where RequireAck
	// is set, and at most 255 bytes.
	//
	// Distinct from MessageID, which is the runtime's own handle for the send.
	// Where both matter, set both.
	FrameMessageID string
	// RequireAck asks the client to acknowledge this message, which only means
	// anything alongside a FrameMessageID.
	RequireAck bool
	// Message is the payload, carried to the client without being read.
	Message []byte
	// InformClientsOnLoss names connections to notify if this message cannot be
	// delivered.
	InformClientsOnLoss []string
	// MessageID identifies the message to the runtime, as on [OutboundMessage].
	MessageID string
	// Caller is the connection that caused this message to be sent, as on
	// [OutboundMessage].
	Caller string
	// WaitForAck waits for the client to acknowledge this message, as on
	// [OutboundMessage]. Unlike a text message, this composes the request for
	// acknowledgement itself: set RequireAck and a FrameMessageID and the frame
	// asks for one.
	WaitForAck bool
}

OutboundBinaryMessage is one binary message a handler sends to a connected client, framed in the Celerity Binary Message Format before it goes out.

The caller gives the parts rather than the bytes. A client reads every binary frame that is not a reserved one as a framed message, so unframed bytes are read as a route length and a route, leaving the application a payload short by its own invented header under a route nothing serves. Laying that out is not something a caller should be doing by hand.

type OutboundMessage

type OutboundMessage struct {
	ConnectionID string
	// Message is sent to the socket exactly as given, as a text frame.
	//
	// Bytes go through [BinarySender], not here, binary frames are a capability
	// of the transport rather than of the SDK, and a managed WebSocket gateway
	// carries text frames only.
	Message []byte
	// InformClientsOnLoss names connections to notify if this message cannot be
	// delivered.
	InformClientsOnLoss []string
	// MessageID identifies the message to the runtime, for acknowledgement
	// between nodes and in loss events. Generated by the runtime when empty,
	// which is the right choice unless the caller needs to match a loss event
	// back to what it sent.
	//
	// Not the id the client sees: the runtime sends Message to the socket
	// exactly as given, so a client-visible id belongs in the payload.
	MessageID string
	// Caller is the connection that caused this message to be sent, carried
	// through to the loss event. Only meaningful alongside InformClientsOnLoss.
	Caller string
	// WaitForAck waits for the client to acknowledge this message, rather than
	// returning once it has been written to the socket, and reports it as
	// failed where the client never does.
	//
	// Message has to ask the client for an acknowledgement in what it carries,
	// which the runtime does not compose. Setting this for one that asks
	// nothing waits until the runtime declares the message lost.
	//
	// Under the Celerity runtime only. A serverless adapter pushes through the
	// provider's own API, which reports whether it accepted the message and
	// nothing about what the client made of it.
	WaitForAck bool
}

OutboundMessage is one message a handler sends to a connected WebSocket client.

type Params

type Params map[string]Values

Params maps a parameter or header name to its ordered values.

func (Params) Add

func (p Params) Add(name, value string)

Add appends a value under name, preserving order.

func (Params) Get

func (p Params) Get(name string) string

Get returns the first value registered under name.

func (Params) Set

func (p Params) Set(name, value string)

Set replaces every value under name with a single one.

type RecordFailure

type RecordFailure struct {
	// MessageID is the id the record arrived with, see [BatchResult.Fail].
	MessageID string
	Error     string
}

RecordFailure names one record that could not be processed.

type Request

type Request struct {
	// Method is uppercase and canonical.
	Method string
	// Path is raw and percent-encoded, as received.
	Path string
	// Route is the template this request matched in the router's own form, so a
	// blueprint's /files/{path+} arrives as /files/{*path}.
	Route string
	// PathParams holds one value per segment for a catch-all parameter.
	// Segments are split from the raw path before being percent-decoded, so a
	// segment containing an encoded separator survives intact.
	PathParams  Params
	QueryParams Params
	// Headers have lowercase names.
	Headers  Params
	SourceIP string
	// RequestID identifies this delivery.
	RequestID string
	// Body is opaque. The media type comes from the content-type header.
	Body []byte
}

Request is an HTTP request as a handler sees it.

type Response

type Response struct {
	// Status must be between 100 and 999. A value outside that range is a fault
	// in the handler, and the runtime answers 500 rather than narrowing it.
	Status  int
	Headers Params
	Body    []byte
}

Response is what an HTTP handler answers with.

func JSONAnswer

func JSONAnswer(status int, message string, detail any) *Response

JSONAnswer builds the same body with structured detail alongside the message.

The shape is the one the other language SDKs answer with, so a client written against a Celerity API in any language reads the same body:

{"message": "...", "details": [...]}

Detail that cannot be encoded is dropped rather than failing the answer: a caller is better served by the status and the message than by a 500 raised while explaining a 400.

func JSONMessage

func JSONMessage(status int, message string) *Response

JSONMessage builds the response body this SDK answers with wherever it answers on a handler's behalf, so a refusal from a guard and an error from a handler read the same way to a caller.

func ResponseForError

func ResponseForError(err error) (*Response, bool)

ResponseForError turns an error carrying a status into the response to answer with, and reports whether it carried one.

An error that carries no status returns false rather than a 500. The caller is then free to report it as an unhandled failure, which is what keeps a fault visible to the runtime's error reporting instead of being flattened into an ordinary answer.

type Result

type Result struct {
	ID string

	HTTP      *Response
	WebSocket *Ack
	Consumer  *BatchResult
	Schedule  *Ack
	Custom    *CustomInvokeResult
	// Error is set when user code returned or panicked with an unhandled error.
	Error *Error
}

Result is the outcome of one dispatch, carrying exactly one outcome.

type ScheduleTrigger

type ScheduleTrigger struct {
	ScheduleID string
	MessageID  string
	// Schedule is the expression the rule was declared with.
	Schedule string
	Input    []byte
	Vendor   []byte
}

ScheduleTrigger is a scheduled rule firing.

type SendError

type SendError struct {
	Failures []SendFailure
}

SendError reports which messages in a send failed.

func (*SendError) Error

func (e *SendError) Error() string

func (*SendError) Failed

func (e *SendError) Failed(index int) bool

Failed reports whether the message at index failed.

type SendFailure

type SendFailure struct {
	Index        int
	ConnectionID string
	Error        string
}

SendFailure names one message that could not be delivered, by its index in the batch that was sent.

type StatusCoder

type StatusCoder interface {
	error
	// StatusCode returns the HTTP status this error should be answered with.
	StatusCode() int
}

StatusCoder is implemented by an error that knows the status it should be answered with.

It is an interface as well as a concrete type so that an application's own error types can carry a status without importing this package's constructors, which is how a domain error stays a domain error.

type StatusError

type StatusError struct {
	Status int
	// Message is what the caller is told. It should say what is wrong without
	// saying anything the caller is not entitled to know.
	Message string
	// Detail is structured information about what is wrong, answered alongside
	// the message. Nil where there is nothing to add.
	//
	// It reaches the caller, so it carries what they can act on and nothing
	// about how this is implemented.
	Detail any
	// Err is the underlying cause, kept for logging and errors.Is/As. It is
	// never sent to the caller.
	Err error
}

StatusError is an error carrying the status and message to answer with.

func BadRequest

func BadRequest(message string) *StatusError

The statuses an API reaches for most often. Anything else is Status.

func Conflict

func Conflict(message string) *StatusError

func Forbidden

func Forbidden(message string) *StatusError

func Gone

func Gone(message string) *StatusError

func NotFound

func NotFound(message string) *StatusError

func NotImplemented

func NotImplemented(message string) *StatusError

func ServiceUnavailable

func ServiceUnavailable(message string) *StatusError

func Status

func Status(status int, message string) *StatusError

Status returns an error answered with the given status.

An empty message falls back to the status text, so the caller is never told only a number.

func Statusf

func Statusf(status int, format string, args ...any) *StatusError

Statusf returns an error answered with the given status, formatting the message. The message reaches the caller, so it should carry nothing internal.

func TooManyRequests

func TooManyRequests(message string) *StatusError

func Unauthorized

func Unauthorized(message string) *StatusError

func UnprocessableEntity

func UnprocessableEntity(message string) *StatusError

func Wrap

func Wrap(status int, message string, cause error) *StatusError

Wrap returns an error answered with the given status, keeping cause for logging and for errors.Is and errors.As. The cause is not sent to the caller.

func (*StatusError) Details

func (e *StatusError) Details() any

Details satisfies DetailCarrier.

func (*StatusError) Error

func (e *StatusError) Error() string

func (*StatusError) ErrorType

func (e *StatusError) ErrorType() string

ErrorType classifies this failure for the protocol's error type field, satisfying TypedError.

func (*StatusError) StatusCode

func (e *StatusError) StatusCode() int

StatusCode satisfies StatusCoder.

func (*StatusError) Unwrap

func (e *StatusError) Unwrap() error

Unwrap exposes the cause, so errors.Is and errors.As see through to it.

func (*StatusError) WithDetail

func (e *StatusError) WithDetail(detail any) *StatusError

WithDetail returns a copy carrying structured detail.

type TypedError

type TypedError interface {
	error
	ErrorType() string
}

TypedError names its own failure for the protocol's error type field.

Without it the field carries the Go type, which is informative where an application declared the type and noise where it did not. errors.New produces *errors.errorString, which names how the error was built rather than what went wrong. The runtime prepends this field to the message on its local invoke endpoint, so what is put here is read by whoever called.

type ValidationError

type ValidationError struct {
	// Message summarises the failure. It should name what was being validated,
	// such as the body or the query, rather than repeating the issues.
	Message string
	Issues  []ValidationIssue
	// Err is the underlying cause, reachable through errors.Is and
	// errors.Unwrap. Never answered to the caller, and so deliberately absent
	// from Error, as a validation library's own error text commonly names types
	// and internal paths. Code that wants it in a log unwraps it.
	Err error
}

ValidationError is an input that failed validation, answered as a 400 with its issues as details.

func Invalid

func Invalid(message string, issues ...ValidationIssue) *ValidationError

Invalid returns a validation error carrying the given issues.

func (*ValidationError) Details

func (e *ValidationError) Details() any

Details satisfies DetailCarrier, putting the issues in the answer.

func (*ValidationError) Error

func (e *ValidationError) Error() string

func (*ValidationError) ErrorType

func (e *ValidationError) ErrorType() string

ErrorType classifies this failure for the protocol's error type field, satisfying TypedError.

func (*ValidationError) StatusCode

func (e *ValidationError) StatusCode() int

StatusCode satisfies StatusCoder: a failed validation is the caller's mistake.

func (*ValidationError) Unwrap

func (e *ValidationError) Unwrap() error

func (*ValidationError) ValidationIssues

func (e *ValidationError) ValidationIssues() []ValidationIssue

ValidationIssues satisfies IssueReporter.

type ValidationIssue

type ValidationIssue struct {
	// Code identifies the kind of problem, so a client can branch on it without
	// parsing prose. It is a stable, lowercase identifier.
	Code string `json:"code"`
	// Path names the field, outermost first, and is empty when the problem is
	// with the input as a whole.
	Path []string `json:"path"`
	// Message says what is wrong in terms of the caller's own request.
	Message string `json:"message"`
}

ValidationIssue is one thing wrong with an input.

func Issue

func Issue(code, message string, path ...string) ValidationIssue

Issue is shorthand for one issue about one field.

func IssuesFrom

func IssuesFrom(err error) []ValidationIssue

IssuesFrom converts an error into issues.

An error that reports its own is answered with those. Anything else becomes a single issue naming nothing, since a message this SDK did not compose may describe the implementation and is not repeated to the caller.

func IssuesFromDecode

func IssuesFromDecode(err error) []ValidationIssue

IssuesFromDecode turns a JSON decoding failure into issues a caller can act on.

The standard library's messages name Go types and struct fields, as in "cannot unmarshal string into Go struct field order.total of type float64". That tells a caller nothing they can use and describes the implementation, so the field is kept and the rest is replaced.

type Values

type Values []string

Values holds the ordered values for one parameter or header name.

Order within a name is significant (RFC 9110 section 5.3); order across names is not. Headers and parameters are multi-valued because collapsing them is lossy: RFC 9110 forbids folding two Set-Cookie headers into one comma-separated value.

func (Values) First

func (v Values) First() string

First returns the first value, or the empty string when there are none.

type WebSocketMessage

type WebSocketMessage struct {
	Route        string
	ConnectionID string
	SourceIP     string
	// RequestID identifies this delivery, as distinct from MessageID.
	RequestID string
	Message   []byte
	// IsBinary reports whether this arrived as a binary frame rather than a
	// text one. A byte body alone cannot express the distinction and the
	// WebSocket protocol treats the two frame types as distinct.
	IsBinary bool
	// MessageID is the id the message is known by: the one the client gave it,
	// or one the runtime generated. It is what an acknowledgement names and what
	// a lost message notification refers to.
	MessageID string
}

WebSocketMessage is one message received from a connected client.

type WebSocketSender

type WebSocketSender interface {
	// Send delivers every message and blocks until the outcome is known.
	//
	// For a message that set WaitForAck, the outcome is what its client made of
	// it, so Send is waiting on a round trip rather than on a write.
	//
	// A partial failure returns a [SendError] naming exactly which messages
	// failed, so a caller can retry exactly those. Re-sending a whole batch
	// would redeliver the messages that did arrive, and whether the client can
	// tell those apart depends on the message ID: a client SDK deduplicates by
	// that where a message carries one, and cannot where it does not.
	Send(ctx context.Context, messages ...OutboundMessage) error
}

WebSocketSender pushes messages to connected clients.

Under the Celerity runtime it is the IPC stream's side channel; under a serverless adapter it is the provider's own push API, such as the API Gateway Management API. A handler reaches it through celerity.WebSocketSenderFrom and does not need to know which.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL