bindcore

package
v0.5.34 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package bindcore holds the declarations both transport runtimes need and neither should own: the error model, the uploaded-file value, and the check helpers. Keeping them here is what lets an error cross between the net/http and fasthttp surfaces and still match, and what keeps a model struct naming File from pulling a transport in behind it.

It imports no transport package.

Index

Constants

View Source
const (
	DefaultSocketReadLimit    int64 = 1 << 20
	DefaultSocketIdleTimeout        = 60 * time.Second
	DefaultSocketPingInterval       = 54 * time.Second
	DefaultSocketWriteTimeout       = 10 * time.Second
	DefaultSocketBufferSize         = 4096
)

Socket option defaults. Every one is non-zero: an unset limit is an allocation the peer chooses, and an unset read deadline is a read nothing can interrupt under TinyGo.

View Source
const CBORContentType = "application/cbor"

CBORContentType is the media type CBOR bodies are read and written under.

View Source
const DefaultMaxCBORBodyBytes int64 = 1 << 20

DefaultMaxCBORBodyBytes is the default cap for CBOR body reads (1 MiB), matching the JSON default so switching a client's encoding does not change how much it may send.

View Source
const DefaultMaxMultipartBodyBytes int64 = 1 << 20

DefaultMaxMultipartBodyBytes is the default cap on multipart request bodies (1 MiB). Override with SetMaxMultipartBodyBytes.

View Source
const DefaultMultipartMaxMemory int64 = 32 << 20

DefaultMultipartMaxMemory is how much of a multipart form stays in RAM before file parts spill to temp files. This is not a body size cap; see DefaultMaxMultipartBodyBytes.

View Source
const MaxFormBodyBytes = 10 << 20

MaxFormBodyBytes is the most an urlencoded body may hold, the bound net/http's ParseForm applies.

View Source
const ProblemContentType = "application/problem+json"

ProblemContentType is the media type of an RFC 9457 error document.

Variables

View Source
var (
	ErrNilSocket    = errors.New("httpbind: nil socket")
	ErrSocketClosed = errors.New("httpbind: socket closed")
	// ErrBinaryMessage reports a binary frame on a socket carrying JSON.
	ErrBinaryMessage = errors.New("httpbind: binary message on a JSON socket")
)
View Source
var (
	ErrNilStream    = errors.New("httpbind: nil stream")
	ErrStreamClosed = errors.New("httpbind: stream closed")
)
View Source
var ErrFileTooLarge = errors.New("httpbind: multipart file too large")

ErrFileTooLarge is returned when a single file part exceeds the limit.

View Source
var ErrNoHijacker = errors.New("httpbind: ResponseWriter is not an http.Hijacker; serve through tinygodriver/httpserver")

ErrNoHijacker reports a ResponseWriter that cannot hand over the connection, which under TinyGo means the server is not the one an upgrade needs.

Functions

func AcceptsCBOR added in v0.5.19

func AcceptsCBOR(accept string) bool

AcceptsCBOR reports whether an Accept header value asks for a CBOR response.

Only an explicit application/cbor entry counts. Wildcards do not: a browser sends */* on every navigation, and answering it with CBOR would switch the default response format on clients that never asked. A q=0 entry is a refusal and does not count either. Relative preference between CBOR and JSON beyond that is not weighed — an explicit ask wins.

func AppendFileJSON

func AppendFileJSON(dst []byte, f File) []byte

AppendFileJSON appends an uploaded file the way encoding/json rendered it before generated encoders stopped going through reflection: exported fields in declaration order, with the content base64-encoded.

func BadRequest

func BadRequest(problem Problem, cause ...error) error

BadRequest returns a 400 Bad Request error.

func BindError

func BindError(field, location, message string) error

BindError is returned when binding fails for a specific field/source.

func CheckDate

func CheckDate(s string) bool

CheckDate reports whether s is an ISO date (YYYY-MM-DD / time.DateOnly).

func CheckDateTime

func CheckDateTime(s string) bool

CheckDateTime reports whether s is RFC3339 (fractional seconds accepted).

func CheckEmail

func CheckEmail(s string) bool

CheckEmail reports whether s is a pragmatic (non-RFC5322) email. Empty string returns false; callers skip empty optional fields before calling.

func CheckOriginDefault added in v0.5.4

func CheckOriginDefault(origin, host string) bool

CheckOriginDefault refuses a handshake whose Origin names a host other than the one the request was addressed to, and admits one carrying no Origin.

A socket accepting any origin is cross-site request forgery with a persistent connection, so this is the default rather than the opt-in.

func CheckTime

func CheckTime(s string) bool

CheckTime reports whether s is an ISO time (HH:MM:SS / time.TimeOnly).

func CheckUUID

func CheckUUID(s string) bool

CheckUUID reports whether s is a UUID string (8-4-4-4-12 hex with dashes). Version/variant bits are not enforced.

func Conflict

func Conflict(problem Problem, cause ...error) error

Conflict returns a 409 Conflict error.

func Forbidden

func Forbidden(problem Problem, cause ...error) error

Forbidden returns a 403 Forbidden error.

func Internal

func Internal(err error) error

Internal returns a 500 Internal Server Error that wraps err.

func IsCBORMediaType added in v0.5.19

func IsCBORMediaType(media string) bool

IsCBORMediaType reports whether media is CBOR or a +cbor structured syntax suffix type (RFC 6839), e.g. application/cbor, application/senml+cbor.

func IsJSONMediaType

func IsJSONMediaType(media string) bool

IsJSONMediaType reports whether media is JSON or a +json structured syntax suffix type (RFC 6839), e.g. application/json, application/problem+json, application/vnd.api+json. text/json is also accepted.

func IsMessageTooLarge

func IsMessageTooLarge(err error) bool

IsMessageTooLarge reports the transport-neutral size-limit failures. A caller adds its own transport's error type before consulting this. The text checks read the outermost message, which carries every wrapped one.

func IsNormalClose added in v0.5.27

func IsNormalClose(err error) bool

IsNormalClose reports whether err is the ordinary end of a socket rather than a failure — the peer's normal or going-away close, an already-sent close, or EOF. A handler's read loop tests it to end without routing the close to the error handler, and it answers alike on both transports where a driver's own IsCloseError does not.

func JSONBodyError added in v0.5.21

func JSONBodyError(err error) error

JSONBodyError wraps a structural failure from a binder's inline JSON body walk in the same 400 problems the pre-inline Object split produced, so moving the parse into generated code changed no response body.

func JSONBodyNotObject added in v0.5.21

func JSONBodyNotObject() error

JSONBodyNotObject is the 400 a binder answers when the body decodes to something other than an object — required whenever payload:"*" rest maps are in play, and long the rule for every bound body.

func MatchQueryPair added in v0.5.27

func MatchQueryPair(rawKey, rawValue, key string) (value string, ok, matched bool)

MatchQueryPair decides one raw pair against a wanted key. matched reports that the pair answered the lookup; a pair whose escapes do not decode is treated the way url.ParseQuery treats it — as if it were not there.

func MaxCBORBodyBytes added in v0.5.19

func MaxCBORBodyBytes() int64

MaxCBORBodyBytes returns the effective global CBOR body limit.

func MaxMultipartBodyBytes

func MaxMultipartBodyBytes() int64

MaxMultipartBodyBytes returns the effective global multipart body limit.

func MediaType

func MediaType(ct string) string

MediaType returns the lowercase type/subtype of a Content-Type header value (parameters after ';' are stripped).

func MultipartParseError

func MultipartParseError(err error, tooLarge bool) error

MultipartParseError maps a parse failure to 413 or 400. The caller decides tooLarge, because detecting it needs a transport-specific error type on top of the shared cases IsMessageTooLarge covers.

func NotFound

func NotFound(problem Problem, cause ...error) error

NotFound returns a 404 Not Found error.

func ParseBytes added in v0.5.24

func ParseBytes(s string) ([]byte, error)

ParseBytes decodes a base64 value carried by a query, path, header or cookie into a byte field.

Both base64 alphabets are accepted, padded or not, because a value source is not a document. Two things force that: a query value carrying + arrives as a space unless the client percent-encoded it, and a client putting a blob in a URL will have reached for the URL-safe alphabet anyway. Refusing either would mean the same blob binds from a body and 400s from a query, which is a difference the field never asked for.

The writing side has no such choice to make. A byte field is written as padded standard base64 wherever it is written, so being permissive here widens what is accepted without widening what is produced.

An empty value is an empty blob rather than an error, matching the empty string a byte field encodes to.

func PayloadTooLarge

func PayloadTooLarge(problem Problem, cause ...error) error

PayloadTooLarge returns a 413 Payload Too Large error.

func ProblemResponse

func ProblemResponse(err error) (status int, body []byte, ok bool)

ProblemResponse derives the status and body of the RFC 9457 document for err. ok is false when err is nil and nothing should be written.

Both transport runtimes call this rather than deriving the document themselves, which is what makes their error bytes identical by construction instead of by two implementations agreeing.

func QueryNeedsUnescape added in v0.5.27

func QueryNeedsUnescape(s string) bool

QueryNeedsUnescape reports whether a raw span decodes to something other than itself. The common query — unescaped ASCII keys and values — skips the decode and its allocation entirely.

func Redirect added in v0.5.11

func Redirect(target string, status ...int) error

Redirect returns a value that sends the browser to target. It travels the error return because a caller that returns values rather than holding a ResponseWriter has no other channel, and because a redirect and an error both end the normal response before it starts.

The value is an ordinary error: nothing panics and no control-flow exception is thrown, which is the difference from how a server-function ecosystem built on exceptions expresses this.

status defaults to 303, which is what a page wants after a POST. Pass one of 301, 302, 307, or 308 to choose another; anything else is refused here rather than emitted as a status no client will follow.

func RedirectTarget added in v0.5.11

func RedirectTarget(err error) (string, int, bool)

RedirectTarget reports the location a redirect value carries.

func RegisterNormalClose added in v0.5.27

func RegisterNormalClose(fn func(error) bool)

RegisterNormalClose adds a transport's normal-close rule. Each transport package calls it once from init, so IsNormalClose answers the same on both surfaces for the callback body they share.

func ReportStreamError

func ReportStreamError(err error)

ReportStreamError hands err to the installed handler, if any.

func RestFormAny

func RestFormAny(formBody FormValues, exclude []string) map[string]any

RestFormAny builds map[string]any from the form keys not in exclude.

func RestFormRaw

func RestFormRaw(formBody FormValues, exclude []string) map[string]json.RawMessage

RestFormRaw builds map[string]json.RawMessage from the form keys not in exclude.

func ScanQuery added in v0.5.27

func ScanQuery(raw, key string) (string, bool)

ScanQuery resolves one key straight off the raw query, for the callers that want a single value and would waste a full split on it.

func ServeSocket added in v0.5.4

func ServeSocket[In, Out any](conn MessageConn, opts SocketOptions, fn func(*Socket[In, Out]) error) error

ServeSocket applies the lifecycle to conn, runs fn, and closes the socket whatever fn returns.

Both surfaces call this, so the limits, the pong accounting, the ping cadence and the close handshake are one implementation rather than two. Closing the connection stays with the caller: fasthttp closes it when its upgrade callback returns, and net/http has to be told.

func SetMaxCBORBodyBytes added in v0.5.19

func SetMaxCBORBodyBytes(n int64)

SetMaxCBORBodyBytes sets the global CBOR body size limit.

n > 0  → use n bytes
n <= 0 → restore DefaultMaxCBORBodyBytes (1 MiB)

func SetMaxMultipartBodyBytes

func SetMaxMultipartBodyBytes(n int64)

SetMaxMultipartBodyBytes sets the global multipart body size limit.

n > 0  → use n bytes
n <= 0 → restore DefaultMaxMultipartBodyBytes (1 MiB)

func SetSocketDefaults added in v0.5.4

func SetSocketDefaults(opts SocketOptions)

SetSocketDefaults installs the process-wide socket options. It is shared with the fasthttp runtime, so installing them once covers both.

func SetStreamErrorHandler

func SetStreamErrorHandler(fn func(error))

SetStreamErrorHandler installs the destination for post-commit stream failures. Passing nil discards them, which is the default: a runtime logging on its own would write to a destination the caller did not choose.

func SocketHandshakeError added in v0.5.4

func SocketHandshakeError(status int, reason error) error

SocketHandshakeError shapes an upgrader's refusal as a Problem-carrying error, so a refused socket looks like every other refusal in the application rather than like the driver's own plain text. Both surfaces install it on their upgrader's Error hook, which is the only place a refusal can be reshaped: the driver writes the response itself, before any 101.

The 500 case keeps its named code for the returned error and the log only; ProblemResponse hides a 5xx code from the body, which is what should happen to a server misconfiguration.

func StatusText

func StatusText(status int) string

StatusText mirrors net/http.StatusText for the codes this package can produce and the ordinary ones beside them, and returns "" for anything else exactly as the standard library does. It exists so the error model links no transport: every constructor below sets Title, so this is reached only by a hand-built HTTPError.

func Unauthorized

func Unauthorized(problem Problem, cause ...error) error

Unauthorized returns a 401 Unauthorized error.

func ValidateSocketOptions added in v0.5.4

func ValidateSocketOptions(opts SocketOptions) error

ValidateSocketOptions rejects a combination that would serve a socket which dies on schedule.

func Validation

func Validation(fields ...FieldError) error

Validation returns a 400 Bad Request validation error with field details.

Types

type FieldError

type FieldError struct {
	Field    string
	Location string
	Message  string
}

FieldError describes a single field-level validation failure.

func Field

func Field(field, location, message string) FieldError

Field builds a field-level validation error.

type File

type File struct {
	Filename    string
	ContentType string
	Size        int64
	Content     []byte
}

File is an uploaded file bound from a multipart/form-data part. After a successful bind, Filename, ContentType (when the client sent one), Size, and Content are populated from the named file part.

func FileFromHeader

func FileFromHeader(fh *multipart.FileHeader, limit int64) (File, error)

FileFromHeader reads one multipart part into a File, bounded by limit. Both transports reach this: fasthttp's MultipartForm also yields *multipart.FileHeader, so the part-reading rule is written once.

func (File) Empty

func (f File) Empty() bool

Empty reports whether f has no filename and no content.

type FormValues added in v0.5.34

type FormValues struct {
	// contains filtered or unexported fields
}

FormValues is a form body read once. An urlencoded body is the same key=value spans a query string is, split the same way and unescaped only when a key is actually looked up, so the url.Values map, its per-key slices and the copy of every member that net/http's ParseForm built are not built at all. A multipart body keeps the map its parser produced, since that parser has already paid for the copies.

The zero value is the absence of a form body, which is what a binder asks Present about before reading the fields that may come from one.

func FormFromMap added in v0.5.34

func FormFromMap(m map[string]string) FormValues

FormFromMap wraps the fields of a multipart body.

func FormFromQuery added in v0.5.34

func FormFromQuery(q QueryValues) FormValues

FormFromQuery wraps an urlencoded body already split into pairs.

func ReadFormValues added in v0.5.34

func ReadFormValues(body string) FormValues

ReadFormValues splits an urlencoded body the caller has read into pairs. body must be owned by the caller; the values alias it.

func (FormValues) Lookup added in v0.5.34

func (f FormValues) Lookup(key string) (string, bool)

Lookup returns the first value for key, the way ParseForm's map did.

func (FormValues) Present added in v0.5.34

func (f FormValues) Present() bool

Present reports whether the request carried a form body at all.

func (FormValues) RestAny added in v0.5.34

func (f FormValues) RestAny(exclude []string) map[string]any

RestAny builds map[string]any from the form keys not in exclude.

func (FormValues) RestRaw added in v0.5.34

func (f FormValues) RestRaw(exclude []string) map[string]json.RawMessage

RestRaw builds map[string]json.RawMessage from the form keys not in exclude, each value quoted by the same append helper generated encoders use.

type HTTPError

type HTTPError struct {
	Status  int
	Title   string
	Problem Problem
	Fields  []FieldError
	// Location carries a redirect target. It is empty for every ordinary error,
	// and set only by Redirect, whose value travels the error return because a
	// redirect and an error both end the normal response before it starts.
	Location string
	// contains filtered or unexported fields
}

HTTPError is an HTTP-mapped error with optional RFC 9457 details and cause.

func AsHTTPError

func AsHTTPError(err error) (*HTTPError, bool)

AsHTTPError extracts *HTTPError from err if present.

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) Unwrap

func (e *HTTPError) Unwrap() error

type MessageConn added in v0.5.4

type MessageConn interface {
	ReadMessage() (messageType int, p []byte, err error)
	NextWriter(messageType int) (io.WriteCloser, error)
	WriteControl(messageType int, data []byte, deadline time.Time) error
	SetReadLimit(limit int64)
	SetReadDeadline(t time.Time) error
	SetWriteDeadline(t time.Time) error
	SetPongHandler(h func(appData string) error)
	Subprotocol() string
	Close() error
}

MessageConn is the part of a driver WebSocket connection a typed socket uses. Both driver Conn types satisfy it structurally, with no adapter.

type Problem

type Problem struct {
	Code    string
	Message string
}

Problem is an application error payload carried by status helpers.

type QueryValues added in v0.5.27

type QueryValues struct {
	// contains filtered or unexported fields
}

QueryValues is a request's query string split once into raw key=value spans, in wire order.

Generated binders only ever ask for a key's first value, so the url.Values map — one allocation per key plus an unescaped copy of every member — buys random access nobody uses. Splitting into spans costs one slice, and a span is unescaped only when it is actually looked up and actually escaped: the same trade the binders' inline JSON walk makes for body members.

func ParseQuery added in v0.5.27

func ParseQuery(raw string) QueryValues

ParseQuery splits raw into the pairs a lookup may answer from.

raw must be a string the caller owns. The spans alias it and every value handed back is cut from it, which is what lets the fasthttp surface convert the pooled query bytes once here rather than per field.

func (QueryValues) Lookup added in v0.5.27

func (q QueryValues) Lookup(key string) (string, bool)

Lookup returns the first value for key.

func (QueryValues) LookupAll added in v0.5.27

func (q QueryValues) LookupAll(key string) []string

LookupAll returns every value for key, in the order the URL wrote them.

A repeated key is the array spelling a browser produces: an urlencoded form writes one pair per successful control, so a checkbox group named tag submits tag=a&tag=b. Nothing else is an array here — brackets are ordinary key characters, and a comma is an ordinary value character.

An empty value contributes nothing. A blank control submits its key with no value, so counting one would turn an untouched filter field into an element no user chose, and tag= and a bare tag are indistinguishable anyway.

type Socket added in v0.5.4

type Socket[In, Out any] struct {
	// contains filtered or unexported fields
}

Socket is a typed WebSocket connection carrying one JSON value per message.

Read must be called from one goroutine. Write may be called from any: it takes a lock the control frames share, so a broadcast goroutine cannot interleave its frame with a message or with a lifecycle ping.

func NewSocket added in v0.5.4

func NewSocket[In, Out any](conn MessageConn, opts SocketOptions) *Socket[In, Out]

NewSocket builds a typed socket over conn. ServeSocket is the entry that applies the lifecycle; this exists for tests and for a caller assembling one by hand.

func (*Socket[In, Out]) Close added in v0.5.4

func (s *Socket[In, Out]) Close() error

Close ends the socket with a normal-closure handshake. Idempotent.

It does not close the underlying connection: the entry that opened it does that, so the close frame is always sent before the transport goes away.

A goroutine calling this to end someone else's read loop does not interrupt a read already blocked — nothing can, under TinyGo. The reader returns when the peer answers the close frame, or when its own deadline expires.

func (*Socket[In, Out]) Read added in v0.5.4

func (s *Socket[In, Out]) Read() (In, error)

Read returns the next message decoded into In.

The read deadline is set here rather than once at setup: netdev takes a deadline by value when a read begins, so the bound has to be in place before the call rather than pushed into it afterwards.

A decode failure is returned without closing the socket, because a message this application cannot read is the application's to answer.

func (*Socket[In, Out]) Subprotocol added in v0.5.4

func (s *Socket[In, Out]) Subprotocol() string

Subprotocol returns the subprotocol the handshake negotiated, or "".

func (*Socket[In, Out]) Write added in v0.5.4

func (s *Socket[In, Out]) Write(v Out) error

Write encodes v as one text frame. Safe to call from any goroutine.

type SocketOptions added in v0.5.4

type SocketOptions struct {
	ReadLimit       int64
	IdleTimeout     time.Duration
	PingInterval    time.Duration
	WriteTimeout    time.Duration
	ReadBufferSize  int
	WriteBufferSize int

	// Subprotocols are offered in preference order; the negotiated one is
	// readable from the socket.
	Subprotocols []string

	// EnableCompression asks for permessage-deflate. It is off by default:
	// it pulls flate into the binary, and only no-context-takeover mode is
	// supported, so the saving is smaller than the cost.
	EnableCompression bool

	// CheckOrigin decides whether a handshake is allowed, from the Origin and
	// Host header values. It takes two strings rather than a request so one
	// policy serves both transports, the same reason stream negotiation does.
	// A nil value takes CheckOriginDefault.
	CheckOrigin func(origin, host string) bool
}

SocketOptions configures one socket. A zero field takes the process default installed with SetSocketDefaults, and a process default left zero takes the constant above, so nothing reaches the driver as zero.

func ResolveSocketOptions added in v0.5.4

func ResolveSocketOptions(opts SocketOptions) SocketOptions

ResolveSocketOptions fills opts from the process defaults, then from the constants. Both surfaces call it so one options value means the same thing on either.

func SocketDefaults added in v0.5.4

func SocketDefaults() SocketOptions

SocketDefaults returns the installed process defaults, with every unset field resolved to its constant.

type Stream

type Stream[T any] struct {
	// contains filtered or unexported fields
}

Stream is a typed incremental response stream over any writer.

func NewStream

func NewStream[T any](w io.Writer, format StreamFormat) *Stream[T]

NewStream builds a stream over w. The caller has already sent the headers StreamHeaders lists and the 200 status.

func (*Stream[T]) Close

func (s *Stream[T]) Close() error

Close marks the stream finished. Idempotent. For JSON array format, Close writes the trailing ']' (or "[]" if no Write). SSE and NDJSON do not require a special trailer.

func (*Stream[T]) Format

func (s *Stream[T]) Format() StreamFormat

Format returns the negotiated stream format (sse | ndjson | json-array).

func (*Stream[T]) Write

func (s *Stream[T]) Write(v T) error

Write encodes one event in the negotiated format. Callable many times; does not re-send HTTP status or headers.

Generated encoders terminate each event with a single '\n', which supplies the NDJSON line ending and the first newline of the SSE frame.

type StreamFormat

type StreamFormat string

StreamFormat is the negotiated on-the-wire format for Stream[T].

const (
	// StreamSSE is text/event-stream (data: <json>\n\n).
	StreamSSE StreamFormat = "sse"
	// StreamNDJSON is application/x-ndjson (one JSON object per line).
	// Same family as JSONL / NDJSON; not a single JSON array document.
	StreamNDJSON StreamFormat = "ndjson"
	// StreamJSONArray is application/json as one JSON array document:
	// [obj1,obj2,...] with items appended incrementally and closed by Close.
	StreamJSONArray StreamFormat = "json-array"
)

func NegotiateStream

func NegotiateStream(streamQuery, accept, userAgent string) StreamFormat

NegotiateStream selects SSE, NDJSON, or JSON array using:

  1. the ?stream= query value
  2. Accept
  3. User-Agent heuristics
  4. default NDJSON

It takes the three values as strings so each transport reads them its own way and the decision itself stays in one place.

Note: NDJSON/JSONL (line-delimited objects) is distinct from JSON array (a single [...] document). application/json selects the array form; application/x-ndjson / application/jsonl select NDJSON.

type StreamHeader

type StreamHeader struct{ Name, Value string }

StreamHeader is one response header a format requires.

func StreamHeaders

func StreamHeaders(format StreamFormat) []StreamHeader

StreamHeaders lists the headers a format's response opens with, in order, so both surfaces send the same set. The returned slice is shared and read-only.

Jump to

Keyboard shortcuts

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