moonrpc
A real gRPC implementation for MoonBit — not gRPC-Web. It now serves a real unary gRPC call over a self-built HTTP/2 (h2c) transport: the RFC 7540 frame layer, the stream state machine, complete HPACK (RFC 7541), and connection- and stream-level flow control — the protocol engine is pure and runs on every backend; the socket driver is native.
moon add Lfan-ke/moonrpc✶ The contract at a glance
let server = @net.GrpcServer::new() server.register("/greet.Greeter/SayHello", req => handle(req)) // (bytes) -> bytes server.serve(port=50051) // a real unary gRPC call over self-built h2c // under the hood — a pure, all-backend protocol engine over the frame + HPACK codecs: let engine = H2Server::new() let out = engine.feed(frame) // frames in -> HEADERS + DATA + grpc-status trailers out
§Framing & status
encode_message / decode_message implement gRPC length-prefixed framing; Status is the 17-code grpc-status model; Method renders the /Service/Method path.
let max_message_size : Int = 4 * 1024 * 1024
The default cap on a single received gRPC message (4 MiB, matching gRPC's default MaxRecvMsgSize). A length prefix above this — including one whose 4 bytes decode to a negative Int because the high bit is set — is rejected rather than trusted, so a hostile prefix can neither slice out of bounds nor pin unbounded buffer.
let max_header_list_size : Int = 128 * 1024
The cap on one accumulated header block (HEADERS plus its CONTINUATION frames). Without it a peer could stream endless non-final CONTINUATION frames and grow the buffer without bound (a CONTINUATION flood); 128 KiB is far above any real gRPC request's headers.
fn encode_message(payload : Bytes, compressed? : Bool = false) -> Bytes
Encode a payload as a gRPC *Length-Prefixed-Message*: a 1-byte compression flag, a 4-byte big-endian length, then the payload. This is the framing every gRPC transport shares (gRPC-Web over HTTP/1.1 and real gRPC over HTTP/2 alike).
fn decode_message(data : Bytes) -> (Bool, Bytes)?
Decode one gRPC length-prefixed message from the front of data, returning (compressed, payload), or None if fewer than a full frame is present.
enum Status
The 17 canonical gRPC status codes (grpc-status).
fn Status::code(self : Status) -> Int
The numeric grpc-status code.
fn Status::name(self : Status) -> String
The canonical uppercase status name.
fn Status::from_h2_error(code : Int) -> Status
The status a call ends with when the peer resets its stream instead of sending trailers, from the RST_STREAM error code (gRPC PROTOCOL-HTTP2, "HTTP2 Error Code → Status"). A refused stream was never processed, so it is UNAVAILABLE and safe to retry; anything the table does not name is INTERNAL, since the peer broke the transport rather than the call.
fn Status::from_http(code : Int) -> Status
The status a non-200 HTTP response maps to when it carries no grpc-status — a proxy or a plain HTTP server answering on the gRPC port (gRPC http-grpc-status-mapping). Anything outside the table is UNKNOWN.
struct Method
A fully-qualified RPC method: package.Service and the method name.
fn Method::path(self : Method) -> String
The gRPC HTTP/2 :path, i.e. /package.Service/Method.
§Protobuf wire runtime
The pure protobuf binary wire codec: PbWriter / PbReader carry the four proto3 wire types (varint, fixed32/64, length-delimited) plus zigzag for the sint types, with tag packing, unknown-field skipping, and truncation / overflow / group-type rejection on decode.
enum WireType
The four protobuf wire types carried by a field tag's low three bits. The two group types (3 start-group, 4 end-group) are deprecated and unsupported, so from_code rejects them.
impl Show for WireType with fn output(self, logger)
Prints the wire type by its protobuf name rather than its number.
fn WireType::code(self : WireType) -> Int
The wire-type number (the tag's low three bits).
fn WireType::from_code(n : Int) -> WireType?
The wire type for a tag's low three bits, or None for the deprecated group types (3/4) and any out-of-range value.
suberror PbErrorA raised protobuf decode failure: Truncated when the buffer ends inside a field, BadWireType for a group or unknown wire type, Overflow for a varint longer than ten octets, and BadUtf8 for an invalid string field.
impl Show for PbError with fn output(self, logger)
Prints the field and offset the decode failed on.
struct PbWriter
An append-only protobuf message encoder. Field writers append a tag and the field body; to_bytes yields the finished message. Fields are written in the caller's order — protobuf places no ordering requirement on distinct fields, and a message struct's encoder writes them by ascending number by convention.
fn PbWriter::new() -> PbWriter
A fresh, empty message encoder.
fn PbWriter::to_bytes(self : PbWriter) -> Bytes
The bytes written so far.
fn PbWriter::write_varint(self : PbWriter, value : UInt64) -> Unit
Append a base-128 varint (protobuf "Base 128 Varints"): seven bits per octet, little-endian groups, the high bit marking continuation.
fn PbWriter::write_tag( self : PbWriter, field : Int, wire : WireType) -> Unit
Append a field tag: (field_number << 3) | wire_type, itself a varint.
fn PbWriter::write_fixed32(self : PbWriter, v : UInt) -> Unit
Append a little-endian 32-bit fixed value (wire type 5, no tag).
fn PbWriter::write_fixed64(self : PbWriter, v : UInt64) -> Unit
Append a little-endian 64-bit fixed value (wire type 1, no tag).
fn PbWriter::write_len_delim(self : PbWriter, body : Bytes) -> Unit
Append a length-delimited body: a varint length then the raw bytes (wire type 2, no tag).
fn PbWriter::int32(self : PbWriter, field : Int, v : Int) -> Unit
Write an int32 field. Negative values sign-extend to a full ten-octet varint, exactly as the reference implementation encodes them.
fn PbWriter::int64(self : PbWriter, field : Int, v : Int64) -> Unit
Write an int64 field.
fn PbWriter::uint32(self : PbWriter, field : Int, v : UInt) -> Unit
Write a uint32 field.
fn PbWriter::uint64(self : PbWriter, field : Int, v : UInt64) -> Unit
Write a uint64 field.
fn PbWriter::sint32(self : PbWriter, field : Int, v : Int) -> Unit
Write a sint32 field (zigzag-encoded so small-magnitude negatives stay short).
fn PbWriter::sint64(self : PbWriter, field : Int, v : Int64) -> Unit
Write a sint64 field (zigzag-encoded).
fn PbWriter::bool_(self : PbWriter, field : Int, v : Bool) -> Unit
Write a bool field.
fn PbWriter::enum_(self : PbWriter, field : Int, v : Int) -> Unit
Write an enum field (its integer value, as a varint).
fn PbWriter::fixed32(self : PbWriter, field : Int, v : UInt) -> Unit
Write a fixed32/sfixed32/float field.
fn PbWriter::fixed64(self : PbWriter, field : Int, v : UInt64) -> Unit
Write a fixed64/sfixed64/double field.
fn PbWriter::bytes_(self : PbWriter, field : Int, v : Bytes) -> Unit
Write a bytes field.
fn PbWriter::string_(self : PbWriter, field : Int, v : String) -> Unit
Write a string field (UTF-8 encoded).
fn PbWriter::message_(self : PbWriter, field : Int, v : Bytes) -> Unit
Write an embedded-message field: the pre-encoded sub-message as a length-delimited body.
struct PbReader
A forward cursor over an encoded protobuf message. read_tag pulls the next field's number and wire type; the typed readers then consume its body. skip discards an unknown field's body so a decoder tolerates fields it does not know (protobuf forward compatibility).
fn PbReader::new(data : Bytes) -> PbReader
A reader positioned at the start of data.
fn PbReader::eof(self : PbReader) -> Bool
Whether the whole message has been consumed.
fn PbReader::read_varint(self : PbReader) -> UInt64 raise PbError
Read a base-128 varint. Raises Overflow past ten octets and Truncated if the buffer ends mid-varint.
fn PbReader::read_tag(self : PbReader) -> (Int, WireType) raise PbError
Read a field tag, returning (field_number, wire_type). Raises BadWireType for a group or unknown wire type.
fn PbReader::read_fixed32(self : PbReader) -> UInt raise PbError
Read a little-endian 32-bit fixed value.
fn PbReader::read_fixed64(self : PbReader) -> UInt64 raise PbError
Read a little-endian 64-bit fixed value.
fn PbReader::read_len_delim(self : PbReader) -> Bytes raise PbError
Read a length-delimited body's raw bytes.
fn PbReader::read_int32(self : PbReader) -> Int raise PbError
Read an int32 field body (the low 32 bits of the varint).
fn PbReader::read_int64(self : PbReader) -> Int64 raise PbError
Read an int64 field body.
fn PbReader::read_uint32(self : PbReader) -> UInt raise PbError
Read a uint32 field body.
fn PbReader::read_uint64(self : PbReader) -> UInt64 raise PbError
Read a uint64 field body.
fn PbReader::read_sint32(self : PbReader) -> Int raise PbError
Read a sint32 field body (zigzag-decoded).
fn PbReader::read_sint64(self : PbReader) -> Int64 raise PbError
Read a sint64 field body (zigzag-decoded).
fn PbReader::read_bool(self : PbReader) -> Bool raise PbError
Read a bool field body.
fn PbReader::read_bytes(self : PbReader) -> Bytes raise PbError
Read a bytes field body.
fn PbReader::read_string(self : PbReader) -> String raise PbError
Read a string field body, decoding UTF-8. Raises BadUtf8 on invalid bytes.
fn PbReader::skip(self : PbReader, wire : WireType) -> Unit raise PbError
Discard the body of a field whose number the decoder does not recognise, given its wire type — the mechanism behind protobuf's forward compatibility.
fn PbWriter::map_string_string( self : PbWriter, field : Int, key : String, value : String) -> Unit
Write one map<string, string> entry for field: a length-delimited submessage { key = 1, value = 2 }, emitted once per pair (a proto map is repeated entries).
fn PbWriter::packed_int32( self : PbWriter, field : Int, values : Array[Int]) -> Unit
Write a packed repeated int32 field: all values as back-to-back varints inside one length-delimited field (proto3's default for scalar repeated).
fn PbReader::read_map_string_string( self : PbReader) -> (String, String) raise PbError
Read one map<string, string> entry (a length-delimited { key = 1, value = 2 } submessage) as (key, value). Call once the tag for the map field has been read.
fn PbReader::read_packed_int32(self : PbReader) -> Array[Int] raise PbError
Read a packed repeated int32 field: the length-delimited body decoded as back-to-back int32 varints.
§Descriptor model
The descriptor model for services and messages and its codec to the FileDescriptorProto / FileDescriptorSet wire bytes of descriptor.proto — the unit Server Reflection returns, encoding from a programmatic model and decoding a protoc-produced FileDescriptorSet through the same types.
enum FieldType
The FieldDescriptorProto.Type enum (proto3's scalar and composite field types). TypeGroup is intentionally absent — groups are removed from proto3.
fn FieldType::code(self : FieldType) -> Int
The descriptor.proto enum number of a field type.
fn FieldType::from_code(n : Int) -> FieldType
The field type for a descriptor.proto enum number; an unrecognised number (including the removed group type 10) reads as TypeMessage.
enum FieldLabel
The FieldDescriptorProto.Label: proto3 fields are LabelOptional unless repeated.
fn FieldLabel::code(self : FieldLabel) -> Int
The descriptor.proto enum number of a label.
fn FieldLabel::from_code(n : Int) -> FieldLabel
The label for a descriptor.proto enum number; anything else reads as LabelOptional.
struct FieldDescriptor
One field of a message (FieldDescriptorProto). type_name is the fully-qualified name of the referenced message or enum for TypeMessage / TypeEnum, and empty for scalars.
fn FieldDescriptor::scalar( name : String, number : Int, type_ : FieldType, label? : FieldLabel = LabelOptional) -> FieldDescriptor
A field with a scalar type and no composite type_name.
fn FieldDescriptor::encode(self : FieldDescriptor) -> Bytes
Encode a FieldDescriptorProto.
fn FieldDescriptor::decode(body : Bytes) -> FieldDescriptor raise PbError
Decode a FieldDescriptorProto.
struct MessageDescriptor
A message type (DescriptorProto): its (simple) name and its fields.
fn MessageDescriptor::encode(self : MessageDescriptor) -> Bytes
Encode a DescriptorProto.
fn MessageDescriptor::decode( body : Bytes) -> MessageDescriptor raise PbError
Decode a DescriptorProto.
struct MethodDescriptor
One RPC method (MethodDescriptorProto): its name, the fully-qualified request and response message names, and the two streaming flags that together pick the call cardinality.
fn MethodDescriptor::encode(self : MethodDescriptor) -> Bytes
Encode a MethodDescriptorProto.
fn MethodDescriptor::decode(body : Bytes) -> MethodDescriptor raise PbError
Decode a MethodDescriptorProto.
struct ServiceDescriptor
A service (ServiceDescriptorProto): its (simple) name and its methods.
fn ServiceDescriptor::encode(self : ServiceDescriptor) -> Bytes
Encode a ServiceDescriptorProto.
fn ServiceDescriptor::decode( body : Bytes) -> ServiceDescriptor raise PbError
Decode a ServiceDescriptorProto.
struct FileDescriptor
A single .proto file (FileDescriptorProto): its filename, package, the message and service types it defines, and the syntax level. This is the unit Server Reflection returns.
fn FileDescriptor::new( name : String, package_ : String, messages? : Array[MessageDescriptor] = [], services? : Array[ServiceDescriptor] = []) -> FileDescriptor
A proto3 file with the given filename and package.
fn FileDescriptor::encode(self : FileDescriptor) -> Bytes
Encode a FileDescriptorProto.
fn FileDescriptor::decode(body : Bytes) -> FileDescriptor raise PbError
Decode a FileDescriptorProto.
fn FileDescriptor::symbols(self : FileDescriptor) -> Array[String]
The fully-qualified names this file defines: package.Service for each service and package.Message for each message. These are the symbols a FileContainingSymbol reflection request can resolve to this file.
fn FileDescriptor::service_names(self : FileDescriptor) -> Array[String]
The fully-qualified names of the services this file defines (package.Service).
fn encode_file_descriptor_set(files : Array[FileDescriptor]) -> Bytes
Encode a FileDescriptorSet (protoc --descriptor_set_out): the concatenation of FileDescriptorProtos under repeated field 1.
fn decode_file_descriptor_set( body : Bytes) -> Array[FileDescriptor] raise PbError
Decode a FileDescriptorSet into its files.
§HPACK primitives
The RFC 7541 header-compression primitives: the 61-entry static table, the prefix-integer representation (§5.1), and non-Huffman string literals (§5.2).
fn hpack_static_table() -> Array[(String, String)]
The 61-entry HPACK static header table (RFC 7541, Appendix A) as (name, value) pairs in RFC index order, i.e. result[0] is index 1 (:authority) and result[60] is index 61 (www-authenticate).
fn hpack_static_entry(index : Int) -> (String, String)?
Look up an HPACK static-table entry by its 1-based RFC index (1..=61), returning (name, value) or None when the index is out of range.
fn hpack_encode_int(value : Int, prefix_bits : Int) -> Bytes
Encode value as an HPACK integer with an prefix_bits-bit prefix (RFC 7541 §5.1). The high 8 - prefix_bits bits of the first octet are left zero for the caller to OR in any flag bits. Examples: 10 on a 5-bit prefix is [0x0A]; 1337 on a 5-bit prefix is [0x1F, 0x9A, 0x0A].
fn hpack_decode_int( data : Bytes, offset : Int, prefix_bits : Int) -> (Int, Int) raise HpackError
Decode an HPACK integer with an prefix_bits-bit prefix from data starting at offset (RFC 7541 §5.1), returning (value, bytes_consumed). Any flag bits above the prefix in the first octet are masked off and ignored.
fn hpack_string_is_huffman(data : Bytes, offset : Int) -> Bool
Whether the string literal at offset is Huffman-coded, i.e. the H bit (the top bit of the length octet) is set (RFC 7541 §5.2).
fn hpack_encode_string(octets : Bytes) -> Bytes
Encode octets as a non-Huffman HPACK string literal (RFC 7541 §5.2): the length as a 7-bit-prefix integer with the H bit clear, followed by the raw octets.
fn hpack_decode_string( data : Bytes, offset : Int) -> (Bytes, Int) raise HpackError
Decode an HPACK string literal from data at offset, returning (octets, bytes_consumed). The length is read as a 7-bit-prefix integer (the H bit is masked off); this is the inverse of hpack_encode_string for H = 0. Use hpack_string_is_huffman first if the literal may be Huffman-coded, as Huffman decoding is not applied here.
§HPACK Huffman coding
The RFC 7541 Appendix B canonical Huffman code table with a prefix-trie decoder and an EOS-padding encoder (§5.2).
suberror HpackErrorA raised HPACK failure (Huffman decoding or header-block decoding).
impl Show for HpackError with fn output(self, logger)
Prints the header block and the offset it gave up at.
fn huffman_encode(input : Bytes) -> Bytes
Huffman-encode input (RFC 7541 §5.2): each octet becomes its code, and the final partial octet is padded with the most-significant bits of the EOS code (all ones). The inverse of huffman_decode for valid inputs.
fn huffman_encoded_length(input : Bytes) -> Int
The number of octets input occupies when Huffman-encoded, without building the output — used to choose the shorter of raw vs. Huffman string literals.
fn huffman_decode(input : Bytes) -> Bytes raise HpackError
Huffman-decode input (RFC 7541 §5.2). Raises HuffmanError if the input contains the EOS symbol, if the trailing padding is not a run of fewer than 8 one-bits, or if the bit stream leaves the code space. The inverse of huffman_encode.
§HPACK dynamic table & codec
The size-bounded dynamic table with eviction (§4), the six header-field representations (§6), and a stateful HpackEncoder / HpackDecoder pair.
struct Header
A decoded header field: name and value as raw octet strings (HTTP/2 header names and values are byte sequences, and gRPC -bin metadata is binary).
fn hpack_encode_string_huffman(octets : Bytes) -> Bytes
Encode octets as a Huffman-coded HPACK string literal (RFC 7541 §5.2): the H bit set, the Huffman length as a 7-bit-prefix integer, then the code.
fn hpack_encode_string_auto(octets : Bytes) -> Bytes
Encode octets as an HPACK string literal, choosing the shorter of the raw (H = 0) and Huffman (H = 1) forms — the standard encoder heuristic.
fn hpack_read_string( data : Bytes, offset : Int) -> (Bytes, Int) raise HpackError
Read an HPACK string literal at offset, resolving Huffman coding when the H bit is set, returning (octets, bytes_consumed). Unlike hpack_decode_string, this applies Huffman decoding. Raises on bad Huffman.
struct DynamicTable
The HPACK dynamic table: a FIFO of recently seen (name, value) entries, newest first (entries[0]), bounded by max_size octets where each entry costs name.len + value.len + 32 (RFC 7541 §4.1). Adding evicts the oldest entries until the newcomer fits; an entry larger than max_size empties the table and is not stored (§4.4).
fn DynamicTable::new(max_size? : Int = 4096) -> DynamicTable
A new empty dynamic table bounded by max_size octets (default 4096, the HTTP/2 initial SETTINGS_HEADER_TABLE_SIZE).
fn DynamicTable::set_max_size(self : DynamicTable, new_max : Int) -> Unit
Resize the table (a dynamic table size update, RFC 7541 §4.2), evicting to fit.
fn DynamicTable::add( self : DynamicTable, name : Bytes, value : Bytes) -> Unit
Insert (name, value) at the front, evicting oldest entries to make room. If the entry alone exceeds max_size the table ends up empty (RFC 7541 §4.4).
fn DynamicTable::count(self : DynamicTable) -> Int
The number of entries currently in the dynamic table.
fn DynamicTable::current_size(self : DynamicTable) -> Int
The current total size of the dynamic table in octets (§4.1 accounting).
fn hpack_encode_size_update(new_max : Int) -> Bytes
Encode a dynamic table size update (RFC 7541 §6.3): 001 prefix with the new maximum size as a 5-bit-prefix integer.
struct HpackDecoder
A stateful HPACK decoder: it owns a dynamic table that persists across the header blocks of a connection. limit is the peer-agreed hard cap (SETTINGS_HEADER_TABLE_SIZE) a size update may not exceed.
fn HpackDecoder::new(max_size? : Int = 4096) -> HpackDecoder
A new decoder whose dynamic table is bounded by max_size octets (also the hard cap enforced on dynamic table size updates).
fn HpackDecoder::decode( self : HpackDecoder, block : Bytes) -> Array[Header] raise HpackError
Decode one complete header block into its header list (RFC 7541 §6), mutating the dynamic table for incrementally indexed fields and size updates. Raises HpackDecodeError/HuffmanError on any malformed representation.
struct HpackEncoder
A stateful HPACK encoder: it owns a dynamic table mirroring the decoder's, and prefers indexed representations. huffman selects Huffman string literals when they are shorter.
fn HpackEncoder::new( max_size? : Int = 4096, huffman? : Bool = true) -> HpackEncoder
A new encoder bounded by max_size octets; huffman (default true) enables the shorter-of-two string-literal heuristic.
fn HpackEncoder::encode( self : HpackEncoder, headers : Array[Header]) -> Bytes
Encode a header list into a header block (RFC 7541 §6), using indexed fields where possible and literal-with-incremental-indexing otherwise (mutating the dynamic table to mirror what the peer decoder will build). The output decodes back to the same header list via HpackDecoder.
§HTTP/2 frame layer
The RFC 7540 frame codec: the 9-octet header and all ten frame types (DATA / HEADERS / PRIORITY / RST_STREAM / SETTINGS / PUSH_PROMISE / PING / GOAWAY / WINDOW_UPDATE / CONTINUATION) with their flags and payloads.
let frame_data : Int = 0x0
HTTP/2 frame type codes (RFC 7540 §6).
let frame_headers : Int = 0x1
HEADERS: opens a stream and carries its HPACK header block.
let frame_priority : Int = 0x2
PRIORITY: the deprecated stream-dependency hint; accepted and ignored.
let frame_rst_stream : Int = 0x3
RST_STREAM: abort one stream without touching the connection.
let frame_settings : Int = 0x4
SETTINGS: the connection parameters each peer announces, and their ack.
let frame_push_promise : Int = 0x5
PUSH_PROMISE: server push, which gRPC never uses.
let frame_ping : Int = 0x6
PING: the round-trip probe keepalive is built on.
let frame_goaway : Int = 0x7
GOAWAY: stop opening streams, and here is the last id I accepted.
let frame_window_update : Int = 0x8
WINDOW_UPDATE: hand the peer more flow-control credit.
let frame_continuation : Int = 0x9
CONTINUATION: the rest of a header block too large for one frame.
let flag_end_stream : Int = 0x1
HTTP/2 frame flags (RFC 7540 §6). Flags are type-specific; the same bit carries different meaning per frame type, hence the shared numeric values.
let flag_ack : Int = 0x1
ACK on SETTINGS and PING shares bit 0x1 with END_STREAM.
let flag_end_headers : Int = 0x4
END_HEADERS: no CONTINUATION follows this header block.
let flag_padded : Int = 0x8
PADDED: a length byte and that many padding bytes wrap the payload.
let flag_priority : Int = 0x20
PRIORITY: this HEADERS frame carries a stream-dependency block.
let settings_header_table_size : Int = 0x1
SETTINGS parameter identifiers (RFC 7540 §6.5.2).
let settings_enable_push : Int = 0x2
SETTINGS_ENABLE_PUSH: whether the peer may send PUSH_PROMISE.
let settings_max_concurrent_streams : Int = 0x3
SETTINGS_MAX_CONCURRENT_STREAMS: how many streams may be open at once.
let settings_initial_window_size : Int = 0x4
SETTINGS_INITIAL_WINDOW_SIZE: the flow-control credit a new stream starts with.
let settings_max_frame_size : Int = 0x5
SETTINGS_MAX_FRAME_SIZE: the largest payload the peer will accept in one frame.
let settings_max_header_list_size : Int = 0x6
SETTINGS_MAX_HEADER_LIST_SIZE: the advisory ceiling on a decoded header list.
let error_no_error : Int = 0x0
HTTP/2 error codes (RFC 7540 §7), carried by RST_STREAM and GOAWAY.
let error_protocol_error : Int = 0x1
PROTOCOL_ERROR: the peer broke the protocol in a way not covered below.
let error_internal_error : Int = 0x2
INTERNAL_ERROR: the endpoint failed for reasons of its own.
let error_flow_control_error : Int = 0x3
FLOW_CONTROL_ERROR: the peer sent more than its credit allowed.
let error_settings_timeout : Int = 0x4
SETTINGS_TIMEOUT: a SETTINGS frame went unacknowledged too long.
let error_stream_closed : Int = 0x5
STREAM_CLOSED: a frame arrived for a stream that was already finished.
let error_frame_size_error : Int = 0x6
FRAME_SIZE_ERROR: the frame's length is invalid for its type.
let error_refused_stream : Int = 0x7
REFUSED_STREAM: the stream was declined before any processing, so it is safe to retry.
let error_cancel : Int = 0x8
CANCEL: the stream is no longer wanted.
let error_compression_error : Int = 0x9
COMPRESSION_ERROR: the HPACK context is corrupt, which kills the connection.
let error_connect_error : Int = 0xA
CONNECT_ERROR: the TCP connection behind a CONNECT stream failed.
let error_enhance_your_calm : Int = 0xB
ENHANCE_YOUR_CALM: the peer is generating too much load.
let error_inadequate_security : Int = 0xC
INADEQUATE_SECURITY: the transport does not meet the minimum this endpoint requires.
let error_http_1_1_required : Int = 0xD
HTTP_1_1_REQUIRED: the request cannot be served over HTTP/2.
struct Priority
The stream-priority block shared by PRIORITY frames and the optional priority section of HEADERS (RFC 7540 §5.3.2). weight is the raw wire byte (0..255); the effective priority weight is weight + 1 (§6.3).
suberror FrameErrorA raised decode failure. Incomplete is the normal "need more bytes" signal a streaming reader catches to wait for the rest; the others are protocol errors.
impl Show for FrameError with fn output(self, logger)
Frame errors print as the wire fault they are, so a log line names the frame and the field that was wrong.
enum Frame
A decoded HTTP/2 frame (RFC 7540 §6). Each variant carries the semantic payload with padding already stripped; padding is the number of padding bytes to (re)emit. Unknown preserves extension/unrecognised frames verbatim so a reader can forward or ignore them (RFC 7540 §4.1).
struct FrameHeader
The fixed 9-octet frame header (RFC 7540 §4.1): a 24-bit payload length, an 8-bit type, 8-bit flags, a reserved bit, and a 31-bit stream identifier.
fn FrameHeader::encode(self : FrameHeader) -> Bytes
Encode a FrameHeader to its 9 wire octets.
fn decode_frame_header( data : Bytes, offset? : Int = 0) -> FrameHeader raise FrameError
Decode the 9-octet frame header at offset. Raises Incomplete when fewer than 9 octets are available. The reserved bit is masked off the stream id.
fn Frame::frame_type(self : Frame) -> Int
The numeric frame-type code of this frame (RFC 7540 §6).
fn Frame::encode(self : Frame) -> Bytes
Encode this frame to its complete wire representation (9-octet header + payload), the exact inverse of decode_frame.
fn decode_frame( data : Bytes, offset? : Int = 0) -> (Frame, Int) raise FrameError
Decode exactly one frame at offset, returning (frame, bytes_consumed) where bytes_consumed is 9 + payload_length. Raises Incomplete when the buffer does not yet hold the whole frame, or a protocol error when the payload is malformed for its type. The inverse of Frame::encode.
§HTTP/2 connection preface
The fixed 24-octet client connection preface (RFC 7540 §3.5).
let connection_preface : Bytes = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
The HTTP/2 client connection preface (RFC 7540 §3.5): the fixed, case-sensitive 24-octet sequence PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n a client sends before its first frame, which must be a SETTINGS frame. Its bytes deliberately form a malformed HTTP/1.1 request so an HTTP/1.1-only server rejects it cleanly.
fn has_connection_preface(data : Bytes) -> Bool
Whether data begins with the exact 24-octet HTTP/2 connection preface.
§HTTP/2 stream state machine
The RFC 7540 §5.1 stream lifecycle (idle / open / half-closed / closed) and the §5.1.1 stream-identifier parity rules.
enum StreamState
The lifecycle state of a single HTTP/2 stream (RFC 7540 §5.1).
impl Show for StreamState with fn output(self, logger)
Prints the RFC 7540 §5.1 state name.
enum StreamEvent
A state-changing stream event: the arrival or departure of the frames that drive §5.1 transitions. Headers/Data carry the END_STREAM flag; Reserve is a PUSH_PROMISE reserving this (promised) stream. Frames that never change stream state (PRIORITY, WINDOW_UPDATE, SETTINGS, PING) are intentionally absent.
suberror StreamErrorAn illegal stream transition (RFC 7540 §5.1): a frame not permitted in the current state (typically a STREAM_CLOSED or PROTOCOL_ERROR condition).
impl Show for StreamError with fn output(self, logger)
Prints which transition was refused.
fn StreamState::on_send( self : StreamState, ev : StreamEvent) -> StreamState raise StreamError
The next state after *sending* ev from this state (RFC 7540 §5.1, local side). Raises InvalidTransition for a frame illegal in the current state.
fn StreamState::on_recv( self : StreamState, ev : StreamEvent) -> StreamState raise StreamError
The next state after *receiving* ev in this state (RFC 7540 §5.1, remote side — the mirror of on_send). Raises InvalidTransition on an illegal frame.
struct Stream
A mutable stream: its identifier and current lifecycle state. send/recv advance the state in place, raising on an illegal transition.
fn Stream::new(id : Int) -> Stream
A fresh idle stream with the given identifier.
fn Stream::send( self : Stream, ev : StreamEvent) -> StreamState raise StreamError
Advance this stream by sending ev, returning the new state.
fn Stream::recv( self : Stream, ev : StreamEvent) -> StreamState raise StreamError
Advance this stream by receiving ev, returning the new state.
fn stream_is_client_initiated(id : Int) -> Bool
Whether id is a client-initiated stream: a non-zero odd identifier.
fn stream_is_server_initiated(id : Int) -> Bool
Whether id is a server-initiated (pushed) stream: a non-zero even identifier.
fn stream_id_valid_for_initiator(id : Int, by_client~ : Bool) -> Bool
Whether a peer that is a client (by_client = true) or server may legally *open* stream id: clients use odd ids, servers use even ids, and 0 is the connection control stream, openable by neither (RFC 7540 §5.1.1).
§gRPC server engine
The pure, transport-independent server core: H2Server::feed turns a stream of decoded frames into the frames to send back — driving the stream state machine, the stateful HPACK codec, and connection- and stream-level flow control (RFC 7540 §6.9), routing a completed application/grpc request to a handler of any of the four call kinds and framing each produced message as its own length-prefixed DATA, closed by grpc-status trailers. Exercised in-memory on every backend.
let default_window_size : Int = 65535
The HTTP/2 default flow-control window and initial SETTINGS_INITIAL_WINDOW_SIZE (RFC 7540 §6.9.2): 65 535 octets.
let default_max_frame_size : Int = 16384
The HTTP/2 default (and minimum) SETTINGS_MAX_FRAME_SIZE (RFC 7540 §6.5.2).
let default_max_streams : Int = 100
The SETTINGS_MAX_CONCURRENT_STREAMS the server advertises and enforces unless a caller says otherwise: 100, the value gRPC and nghttp2 both settle on. RFC 9113 §5.1.2 leaves the setting unset by default, which means unbounded — and every live stream holds a request buffer, so a peer that opens streams and never finishes them pins memory for as long as the connection lasts.
struct SrvStream
One server-side stream: its lifecycle state, the accumulating request header block and DATA (with a cursor over the length-prefixed messages already pulled out of it), the per-stream flow-control windows, and the response side — the bytes still to send, whether the initial HEADERS and the trailers have gone out, and any live bidi call state.
struct H2Server
The server side of one HTTP/2 connection: the HPACK codec pair, the live streams, the connection-level flow-control windows, and the peer's settings that bound what we may send. Persistent across the whole connection because HPACK and flow control are stateful.
fn H2Server::new() -> H2Server
A fresh server engine with no registered handlers. Flow-control windows start at the HTTP/2 defaults until the peer's SETTINGS adjust them.
fn H2Server::register( self : H2Server, path : String, handler : (Bytes) -> Bytes) -> Unit
Register a unary handler for a fully-qualified gRPC path (/pkg.Service/Method): one request message in, one reply message out. An unmatched path gets a trailers-only grpc-status: 12 (UNIMPLEMENTED) response.
fn H2Server::release(self : H2Server, id : Int) -> Unit
Drop everything the engine still holds for a finished stream. The map is only ever inserted into otherwise, so a long-lived connection would keep one full request and response per RPC it has ever served. An unknown id is a no-op.
fn H2Server::register_handler( self : H2Server, path : String, handler : Handler) -> Unit
Register a handler of any of the four gRPC call kinds.
fn H2Server::register_unary( self : H2Server, path : String, handler : (RpcContext, Bytes) -> Bytes) -> Unit
Register a unary handler that also sees the call context (metadata, deadline, and the response metadata slots).
fn H2Server::register_server_streaming( self : H2Server, path : String, handler : (RpcContext, Bytes) -> Array[Bytes]) -> Unit
Register a server-streaming handler: one request message, an ordered sequence of reply messages, each framed as its own gRPC message.
fn H2Server::register_client_streaming( self : H2Server, path : String, handler : (RpcContext, Array[Bytes]) -> Bytes) -> Unit
Register a client-streaming handler: every request message the client sends is collected, and after the client half-closes the handler returns one reply.
fn H2Server::register_bidi( self : H2Server, path : String, factory : (RpcContext) -> BidiHandler) -> Unit
Register a bidirectional-streaming handler. The factory runs once per call and returns a BidiHandler whose on_message fires per request message (its replies stream out immediately) and whose on_end fires at half-close.
fn H2Server::goaway_received(self : H2Server) -> Bool
Whether the peer has sent GOAWAY; the driver stops accepting new streams once this is set.
fn H2Server::preface(self : H2Server) -> Array[Frame]
The server's opening frames (RFC 7540 §3.5): a SETTINGS frame disabling server push and naming how many streams this connection will run at once. Sent immediately after the client connection preface is validated, before any request frame is read.
fn H2Server::set_max_streams(self : H2Server, n : Int) -> Unit
Set how many streams this connection may run at once. The value is both advertised as SETTINGS_MAX_CONCURRENT_STREAMS and enforced on arrival, so set it before preface and the peer is told exactly what it will be held to.
fn H2Server::set_clock(self : H2Server, clock : () -> Int64) -> Unit
Install the clock the engine measures grpc-timeout deadlines against: a function returning the current time in milliseconds on any monotonic scale. The engine is a pure state machine with no clock of its own, so until one is installed every deadline is inert; the native driver installs @async.now.
fn H2Server::goaway(self : H2Server, last_stream_id : Int) -> Array[Frame]
Frames for a graceful shutdown: a GOAWAY announcing last_stream_id — the highest client stream the server will still process (RFC 7540 §6.8) — with NO_ERROR, so the peer opens no new streams while in-flight ones finish. The driver writes these before closing the connection. A stream opened above last_stream_id afterwards is declined with RST_STREAM(REFUSED_STREAM), which tells the client it was never processed and can be re-issued elsewhere.
fn H2Server::closing(self : H2Server) -> Bool
Whether a connection error has been reported. The GOAWAY carrying it came back from the feed that found it, so the driver writes that and then closes; the engine processes nothing more (RFC 9113 §5.4.1).
fn H2Server::feed(self : H2Server, frame : Frame) -> Array[Frame]
Feed one decoded incoming frame to the engine, advancing all state and returning the frames to write back (SETTINGS ack, PING pong, WINDOW_UPDATE, and — as the request stream progresses — the framed gRPC response messages and trailers). A protocol fault comes back as the frames that report it rather than as a raise the driver can only discard: RST_STREAM for a stream error, and for a connection error a GOAWAY, after which closing is set and every later frame is ignored (RFC 9113 §5.4).
fn H2Server::tick(self : H2Server) -> Array[Frame]
Expire every call whose deadline has passed, returning the frames that end them. The driver calls this on whatever schedule it keeps time on; the engine also checks the deadline whenever a stream advances, so a call that is making progress never needs a tick to be cut off.
fn H2Server::highest_stream(self : H2Server) -> Int
The highest client stream id this connection has opened, which is what a GOAWAY has to name so the peer knows which of its streams were accepted.
fn H2Server::active_streams(self : H2Server) -> Int
How many streams are still running: the ones RFC 9113 §5.1.2 counts against SETTINGS_MAX_CONCURRENT_STREAMS, which is open and both half-closed states. A graceful shutdown waits for this to reach zero before closing the connection. Finished streams stay in the map with their state but are not counted, and neither is one the engine only knows of because a WINDOW_UPDATE named it — that stream was never opened, so it holds nothing and occupies no slot.
fn H2Server::stream_state(self : H2Server, id : Int) -> StreamState
The lifecycle state of stream id, or Idle if the engine has never seen it.
§Streaming call kinds & context
The four gRPC cardinalities (Unary / ServerStreaming / ClientStreaming / Bidi) and the per-call RpcContext: request metadata, the grpc-timeout deadline, and response initial/trailing metadata the handler can set.
struct RpcContext
The context surfaced to a handler for one RPC call: the invoked :path, the request metadata (custom HEADERS, minus the pseudo- and reserved gRPC headers), the deadline parsed from grpc-timeout (in milliseconds, None when absent), and mutable slots for response initial metadata and trailing metadata the handler can set before it returns.
fn RpcContext::empty() -> RpcContext
A context with no path, metadata, or deadline — the placeholder a stream holds until its request HEADERS are decoded.
fn RpcContext::fail( self : RpcContext, code : Int, message : String) -> Unit
End the call with a non-OK gRPC status. A handler calls this (e.g. ctx.fail(Status::code(NotFound), "user 42 does not exist")) instead of returning a normal reply; the server sends the grpc-status / grpc-message trailer and drops the reply message. The five common codes have named helpers on [Status]; any code is accepted.
fn RpcContext::add_error_detail(self : RpcContext, detail : Bytes) -> Unit
Attach a rich-error detail (a serialized google.protobuf.Any) to a failing call. The server packs the status code, message, and every detail into a google.rpc.Status and sends it base64-encoded in the grpc-status-details-bin trailer — gRPC's rich-error channel. Combine with fail, which sets the base grpc-status / grpc-message.
fn RpcContext::metadata_get(self : RpcContext, name : Bytes) -> Bytes?
The value of request metadata name, or None. Names are matched byte-for-byte (gRPC lowercases header names on the wire), so binary -bin metadata works too.
fn RpcContext::add_header( self : RpcContext, name : Bytes, value : Bytes) -> Unit
Add an initial-metadata header to the response. Only takes effect if called before the response HEADERS are flushed (any point inside a unary / server- / client-streaming handler, or inside a bidi factory before the first message).
fn RpcContext::add_trailer( self : RpcContext, name : Bytes, value : Bytes) -> Unit
Add a trailing-metadata header, sent in the trailer HEADERS alongside grpc-status.
struct BidiHandler
A live bidirectional call. on_message is invoked once per fully-received request message and returns the reply messages to send right then; on_end runs after the client half-closes and returns the final replies. Both feed the same flow-controlled response stream, so responses interleave with requests.
enum Handler
A registered method, in one of gRPC's four cardinalities. The reply shape mirrors the request shape: streaming handlers produce an ordered Array[Bytes] of messages, each framed as its own length-prefixed gRPC message on the wire.
fn parse_grpc_timeout(v : Bytes) -> Int?
Parse a grpc-timeout value (RFC: up to 8 ASCII digits then a unit — H/M/S/m/u/n) to whole milliseconds, flooring sub-millisecond units. None for a malformed value.
§gRPC client engine
The pure client core, symmetric to H2Server: H2Client allocates client stream ids, builds request HEADERS and length-prefixed DATA honouring the send windows, and turns the response frames back into a CallReply — the :status, grpc-status, reply messages, and initial/trailing metadata. Runs in-memory on every backend.
struct ClientCall
One client-side call: its stream id, the request side (the length-prefixed request body still to send, the send window, and whether the request has been half-closed), and the response side (the accumulating DATA with a cursor over the messages already pulled out, the recv window, the captured :status and grpc-status, and the response initial and trailing metadata).
struct CallReply
The completed result of a call: the HTTP :status, the numeric grpc-status (-1 if the peer never sent one), the response initial metadata, the reply messages in order, and the trailing metadata.
struct H2Client
The client side of one HTTP/2 connection: the HPACK codec pair (stateful across every call on the connection), the live calls keyed by stream id, the next odd stream id to allocate (§5.1.1), and the connection-level flow-control state bounded by the peer's SETTINGS.
fn H2Client::new() -> H2Client
A fresh client engine with no live calls. The first call takes stream id 1.
fn H2Client::preface(self : H2Client) -> Array[Frame]
The client's opening frames (RFC 7540 §3.5): a SETTINGS frame disabling server push. Written right after the 24-octet connection preface bytes, before any request.
fn H2Client::open( self : H2Client, path : String, metadata? : Array[Header] = [], authority? : String = "127.0.0.1", timeout_millis? : Int? = None) -> Int
Open a new call on this connection for path (/pkg.Service/Method), returning its freshly allocated stream id. metadata is sent as custom request HEADERS; timeout_millis, when set, becomes the grpc-timeout header. The request body is added with send and half-closed with close_send.
fn H2Client::send( self : H2Client, id : Int, message : Bytes, end? : Bool = false) -> Array[Frame]
Append one request message to a call and return the frames to write now (the request HEADERS the first time, then as much length-prefixed DATA as the send windows allow). Set end on the last message to half-close the request.
fn H2Client::close_send(self : H2Client, id : Int) -> Array[Frame]
Half-close the request side of a call (no more request messages) and return any frames that completes — the trailing END_STREAM.
fn H2Client::unary( self : H2Client, path : String, request : Bytes, metadata? : Array[Header] = [], authority? : String = "127.0.0.1", timeout_millis? : Int? = None) -> (Int, Array[Frame])
Open a unary call and return (stream_id, frames_to_write) in one step: the request HEADERS and the single length-prefixed request message with END_STREAM.
fn H2Client::feed(self : H2Client, frame : Frame) -> Array[Frame] raise
Feed one decoded response frame to the engine, advancing all state and returning the frames to write back (SETTINGS ack, PING pong, WINDOW_UPDATE replenishing a receive window, and — once a WINDOW_UPDATE lifts back-pressure — any remaining request DATA). Captures :status, grpc-status, response metadata, and the reassembled reply messages.
fn H2Client::release(self : H2Client, id : Int) -> Unit
Drop everything the engine still holds for a finished call: its buffered body, messages, header block and trailers. The map is only ever inserted into otherwise, so a long-lived connection would keep one full request and response per RPC it has ever made, and every WINDOW_UPDATE and SETTINGS frame would walk that whole history. An unknown or already-released id is a no-op.
fn H2Client::is_done(self : H2Client, id : Int) -> Bool
Whether a call has fully completed (its response ended). An unknown id counts as done so a driver loop terminates.
fn H2Client::goaway_received(self : H2Client) -> Bool
Whether the peer has sent GOAWAY; a draining connection opens no new stream, so the driver picks a fresh one for further calls. Mirrors H2Server::goaway_received.
fn H2Client::goaway_last_stream_id(self : H2Client) -> Int
The peer's last processed stream id from its GOAWAY (0 if none was seen); a call whose stream id is above it was never handled by the server.
fn H2Client::is_retryable(self : H2Client, id : Int) -> Bool
Whether call id was left retryable by a GOAWAY: its stream id is above the peer's last processed id, so the server never saw it and re-issuing the RPC on a fresh connection is safe (gRPC transport GOAWAY / RFC 7540 §6.8). False for a call at or below that id — it may have been processed — and when no GOAWAY arrived.
fn H2Client::reply(self : H2Client, id : Int) -> CallReply
The completed result of a call. Meaningful once is_done is true.
fn H2Client::has_messages(self : H2Client, id : Int) -> Bool
Whether a call has reply messages buffered but not yet taken — for reading a streaming response incrementally as it arrives, rather than all at once via reply.
fn H2Client::take_messages(self : H2Client, id : Int) -> Array[Bytes]
Take every reply message received so far, clearing the call's buffer — the incremental counterpart to reply, for a server- or bidi-streaming response read message by message.
fn encode_grpc_timeout(millis : Int) -> Bytes
Encode whole millis as a grpc-timeout header value (RFC gRPC HTTP/2 mapping): the m (millisecond) unit when the count fits the 8-digit field, else seconds with the S unit.
fn percent_decode(v : Bytes) -> String
Percent-decode a received grpc-message value — the inverse of the server's percent_encode (gRPC spec §"Responses"): each %XX escape becomes the byte its two hex digits name, every other byte passes through, and a malformed escape is left literal; the result is read as UTF-8.
§Server interceptors
Unary and server-streaming interceptor chains wrapped around a method handler, folded outermost-first: each interceptor sees the context and request, calls next to proceed, or returns without it to short-circuit.
type UnaryInterceptor = (RpcContext, Bytes, (RpcContext, Bytes) -> Bytes) -> Bytes
A unary server interceptor: (ctx, request, next) -> reply, where next is the remainder of the chain. Call next(ctx, request) to proceed, or return without calling it to short-circuit.
type StreamInterceptor = (
A server-streaming interceptor: (ctx, request, next) -> replies. It can pre-process the request, post-process the reply sequence, or short-circuit.
fn H2Server::add_unary_interceptor( self : H2Server, interceptor : UnaryInterceptor) -> Unit
Add a unary interceptor to the server's chain. Applies to every unary method; interceptors run in registration order, outermost first.
fn H2Server::add_stream_interceptor( self : H2Server, interceptor : StreamInterceptor) -> Unit
Add a server-streaming interceptor to the server's chain.
§Health service
The grpc.health.v1.Health service (Check + Watch) with a hand-coded protobuf codec for its two messages and a per-service ServingStatus table.
enum ServingStatus
The grpc.health.v1.HealthCheckResponse.ServingStatus enum.
fn ServingStatus::code(self : ServingStatus) -> Int
The wire value of a serving status (the protobuf enum number).
fn ServingStatus::from_code(n : Int) -> ServingStatus
The serving status for a protobuf enum number; out-of-range numbers read as StatusUnknown.
fn encode_health_request(service : Bytes) -> Bytes
Encode a HealthCheckRequest: field 1 (service, a length-delimited string). An empty service name encodes to the empty message, the wire form of the overall-server check.
fn decode_health_request(msg : Bytes) -> Bytes
Decode the service field of a HealthCheckRequest, or empty bytes when the field is absent (the overall-server check). Unknown fields are skipped.
fn encode_health_response(status : ServingStatus) -> Bytes
Encode a HealthCheckResponse: field 1 (status, a varint enum). The SERVING default 0 still encodes to the empty message per protobuf default-omission.
fn decode_health_response(msg : Bytes) -> ServingStatus
Decode the status field of a HealthCheckResponse; an absent field reads as the 0 default (StatusUnknown).
let health_check_path : String = "/grpc.health.v1.Health/Check"
The gRPC HTTP/2 path of the Check method.
let health_watch_path : String = "/grpc.health.v1.Health/Watch"
The gRPC HTTP/2 path of the Watch method.
struct HealthService
A grpc.health.v1.Health service backed by a per-service status table. The empty key "" is the overall-server status; a fresh service reports the whole server SERVING.
fn HealthService::new() -> HealthService
A health service reporting the overall server as SERVING.
fn HealthService::set_status( self : HealthService, service : String, status : ServingStatus) -> Unit
Set the serving status of a named service (or the overall server with "").
fn HealthService::check( self : HealthService, service : String) -> ServingStatus
The serving status of a named service: its set status, or ServiceUnknown when the service was never registered.
fn HealthService::handlers( self : HealthService) -> Array[(String, Handler)]
The (path, handler) pairs implementing the service: Check as a unary method and Watch as a server-streaming method that emits the current status. A live Watch that also pushes on every later change needs a streaming source the pure engine's eager ServerStreaming shape does not model, so this emits the status at subscribe time — the first message a real Watch always sends.
fn HealthService::install(self : HealthService, server : H2Server) -> Unit
Register Check and Watch on a pure server engine.
§Server Reflection service
The grpc.reflection.v1.ServerReflection service (and its v1alpha alias) as a bidi stream: ListServices enumerates registered services, FileContainingSymbol and FileByFilename return the real FileDescriptorProto bytes, so a reflection client such as grpcurl can list and describe a service.
let reflection_v1_path : String = "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"
The gRPC HTTP/2 path of the v1 reflection stream.
let reflection_v1alpha_path : String = "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo"
The gRPC HTTP/2 path of the legacy v1alpha reflection stream. grpcurl tries v1 first and falls back to this, so a server registers both.
enum ReflectionRequest
A decoded ServerReflectionRequest, reduced to the one message_request oneof arm that was set. Extension-number queries are surfaced as Unsupported, which the service answers with an UNIMPLEMENTED error response.
fn decode_reflection_request( body : Bytes) -> ReflectionRequest raise PbError
Decode a ServerReflectionRequest down to its message_request oneof arm. host (field 1) is accepted and ignored; an unset oneof reads as Unsupported.
fn encode_reflection_request(req : ReflectionRequest) -> Bytes
Encode a ServerReflectionRequest carrying a single oneof arm. Used by an in-process reflection client (and by the round-trip tests).
enum ReflectionResponse
A decoded ServerReflectionResponse, reduced to the one message_response arm.
fn decode_reflection_response( body : Bytes) -> ReflectionResponse raise PbError
Decode a ServerReflectionResponse down to its message_response arm — the half an in-process reflection client (and the tests) needs to read an answer.
struct ReflectionService
A grpc.reflection.v1.ServerReflection service backed by an in-memory descriptor database. Each added FileDescriptor is indexed by filename and by every symbol (package.Service / package.Message) it defines, so a FileContainingSymbol or FileByFilename query resolves to the right file, and ListServices enumerates every registered service.
fn ReflectionService::new() -> ReflectionService
An empty reflection service. Add the descriptors of the services you serve with add_file.
fn ReflectionService::add_file( self : ReflectionService, file : FileDescriptor) -> Unit
Register a file descriptor: index it by filename and by each symbol it defines, and add its services to the ListServices set.
fn ReflectionService::handle( self : ReflectionService, request : Bytes) -> Bytes
Answer one ServerReflectionRequest (raw bytes) with the encoded ServerReflectionResponse. A decode failure or an unknown symbol/filename yields an ErrorResponse rather than raising, since it rides a non-raising stream handler.
fn ReflectionService::handler(self : ReflectionService) -> Handler
The bidi handler backing ServerReflectionInfo: one response per request, none at half-close.
fn ReflectionService::install( self : ReflectionService, server : H2Server) -> Unit
Register ServerReflectionInfo at both the v1 and v1alpha paths on a pure server engine.
§h2c socket transport (native)
The native driver that pumps bytes between a real @socket.Tcp connection and the H2Server engine: GrpcServer registers handlers of every call kind and serves them over the self-built HTTP/2 (h2c) transport. Native-only — real sockets and moonbitlang/async have no JS/Wasm backend.
struct GrpcServer
A gRPC server: a registry of handlers keyed by /pkg.Service/Method path, served over the self-built HTTP/2 (h2c) transport. Handlers may be any of the four gRPC call kinds. Each accepted connection gets its own @moonrpc.H2Server engine (HPACK and flow control are per-connection state).
fn GrpcServer::new() -> GrpcServer
A gRPC server with no registered methods.
fn GrpcServer::register( self : GrpcServer, path : String, handler : (Bytes) -> Bytes) -> Unit
Register a unary handler (request) -> reply (messages without their gRPC length prefix) for a fully-qualified path /pkg.Service/Method.
fn GrpcServer::register_unary( self : GrpcServer, path : String, handler : (@moonrpc.RpcContext, Bytes) -> Bytes) -> Unit
Register a unary handler that receives the [RpcContext], so it can read request metadata / the deadline and end the call with a non-OK status via ctx.fail.
fn GrpcServer::register_server_streaming( self : GrpcServer, path : String, handler : (@moonrpc.RpcContext, Bytes) -> Array[Bytes]) -> Unit
Register a server-streaming handler: one request message in, an ordered sequence of reply messages out.
fn GrpcServer::register_client_streaming( self : GrpcServer, path : String, handler : (@moonrpc.RpcContext, Array[Bytes]) -> Bytes) -> Unit
Register a client-streaming handler: every request message is collected, and after the client half-closes the handler returns one reply.
fn GrpcServer::register_bidi( self : GrpcServer, path : String, factory : (@moonrpc.RpcContext) -> @moonrpc.BidiHandler) -> Unit
Register a bidirectional-streaming handler.
fn GrpcServer::register_reflection( self : GrpcServer, refl : @moonrpc.ReflectionService) -> Unit
Register the grpc.reflection.v1.ServerReflection service (and its v1alpha alias) so a reflection client such as grpcurl can list and describe the services in refl's descriptor database.
async fn GrpcServer::serve( self : GrpcServer, host? : String = "127.0.0.1", port? : Int = 50051) -> Unit
Serve gRPC over h2c on host:port, blocking in the accept loop until the running task is cancelled or graceful_stop drains the server. Each connection is driven by drive; a failure on one connection is isolated and does not stop the server.
async fn GrpcServer::graceful_stop( self : GrpcServer, grace? : Int = 10000) -> Unit
Stop serving without cutting anyone off: stop accepting new connections, let the calls that are already running finish, and send every connection a GOAWAY naming the last stream it opened, so clients know what was accepted and can reconnect elsewhere. Returns once the last connection is gone, or after grace milliseconds — a client that received the GOAWAY and then went quiet without closing cannot be waited on forever. serve returns on its own once the accept loop and the last driver are done, so the task running it ends without being cancelled; if the grace period ran out with connections still open, cancelling that task is what releases them.
§Channel client (native)
The native Channel: a long-lived, multiplexed h2c connection over a real @socket.Tcp, driven by the H2Client engine, performing unary, server- and client-streaming calls and enforcing the grpc-timeout deadline by racing the read loop against a timer.
struct Channel
A connected gRPC channel. One TCP connection carries every call, each on its own client-allocated (odd) stream id, so calls multiplex over the shared HPACK and flow-control state the engine keeps.
async fn Channel::connect(host : String, port : Int) -> Channel
Open a channel to host:port: connect, send the 24-octet connection preface and the client SETTINGS. The server's SETTINGS are read and acknowledged lazily on the first call.
fn Channel::close(self : Channel) -> Unit
Close the underlying connection. Outstanding calls are abandoned.
async fn Channel::unary( self : Channel, path : String, request : Bytes, metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None, retry? : @moonrpc.RetryPolicy? = None) -> @moonrpc.CallReply
Make a unary call: one request message, one reply. timeout_millis, when set, is sent as grpc-timeout and enforced locally. With a retry policy the call is re-issued on a new stream after a backoff sleep while its status is retryable and attempts remain, with the whole retry sequence bounded by the one deadline. A call is retried only while uncommitted — here, while no reply message has arrived; committing on received initial metadata as well (gRPC A6) is a refinement, and matters only for the rare retryable failure that follows a non-trailers-only response.
async fn Channel::server_streaming( self : Channel, path : String, request : Bytes, metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply
Make a server-streaming call: one request message, then read the ordered run of reply messages (in CallReply::messages) until the server closes the stream.
async fn Channel::stream_take( self : Channel, path : String, request : Bytes, count : Int, metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None, half_close? : Bool = true) -> Array[Bytes]
Open a streaming call and collect at least count reply messages read inline off the socket — no background reader — returning as soon as that many have arrived or the server ends the stream. This bounds an otherwise unbounded stream (a subscription such as etcd's Watch, or a long server-streaming feed) to the messages the caller wants, then the caller closes the channel; reading inline keeps the whole exchange in one task with a clean close, avoiding a demux pump that a blocked socket read can't be cancelled out of. Pass half_close=false for a bidi subscription that keeps its request side open.
async fn Channel::client_streaming( self : Channel, path : String, requests : Array[Bytes], metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply
Make a client-streaming call: send every message in requests, half-close, then read the single reply.
async fn Channel::bidi( self : Channel, path : String, requests : Array[Bytes], metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply
Make a bidirectional-streaming call: send every message in requests, half-close, then read the ordered run of reply messages the server produced (in CallReply::messages). This is the eager form the engine's synchronous core models — every request is sent before the replies are collected, which pairs with the eager server BidiHandler. A truly interactive exchange (sending while receiving) or several concurrent calls multiplexed on one channel need the shared read-demux loop on the roadmap; until then a Channel drives one call at a time.
fn GrpcServer::register_health( self : GrpcServer, health : @moonrpc.HealthService) -> Unit
Register the grpc.health.v1.Health service (Check + Watch) on this server.
§Call policy
What a client does around a call rather than inside it: the retry policy and its backoff, hedged attempts across backends, and the keepalive pings that keep an idle connection from being reaped.
struct RetryPolicy
A method's retry policy. max_attempts counts the first try plus every retry (gRPC caps it at 5); a call is retried only while attempts remain and its status is in retryable. Backoff starts at initial_backoff_millis and grows by backoff_multiplier each retry, capped at max_backoff_millis.
fn RetryPolicy::default() -> RetryPolicy
A conventional default: up to 3 attempts, 100 ms initial backoff doubling to a 1 s cap, retrying only UNAVAILABLE — the code a transient, safe-to-retry transport failure carries.
fn RetryPolicy::should_retry( self : RetryPolicy, status_code : Int, attempts_made : Int) -> Bool
Whether a call that has made attempts_made attempts (the first is 1) and got status_code should be tried again: attempts must remain, and the status must be non-OK and listed as retryable.
fn RetryPolicy::backoff_millis( self : RetryPolicy, retry_index : Int) -> Int
The backoff cap before the retry_index-th retry (the first retry is 1): min(initial * multiplier^(retry_index-1), max). gRPC sleeps a value drawn uniformly from [0, cap]; the async driver applies that jitter, so this returns the deterministic upper bound (which is also what a jitter-free driver sleeps).
struct HedgingPolicy
A method's hedging policy. max_attempts counts every parallel attempt (gRPC caps it at 5); a new attempt originates every hedging_delay_millis while attempts remain. A finished attempt commits the whole call unless its status is one of non_fatal, in which case hedging continues.
fn HedgingPolicy::default() -> HedgingPolicy
A conventional default: up to 3 parallel attempts, a fresh one every 500 ms, treating only UNAVAILABLE as non-fatal — so a transient transport failure keeps hedging while a real application error commits at once.
fn HedgingPolicy::attempt_cap(self : HedgingPolicy) -> Int
The effective attempt cap: the configured max_attempts, but never above gRPC's ceiling of 5.
fn HedgingPolicy::can_start_another( self : HedgingPolicy, attempts_started : Int) -> Bool
Whether another hedged attempt may originate given attempts_started (the number already launched): only while attempts remain under the cap.
fn HedgingPolicy::should_commit( self : HedgingPolicy, status_code : Int) -> Bool
Whether a finished attempt with status_code commits the whole call — stopping hedging and returning this result. OK commits (success), and so does any error *not* listed as non-fatal (a real failure retrying won't fix). A non-fatal error does not commit; hedging continues.
fn HedgingPolicy::delay_millis(self : HedgingPolicy) -> Int
The delay between originating hedged attempts. The async driver waits this long before launching the next parallel attempt; 0 fires them all at once.
struct Keepalive
Keepalive state for one connection: the idle threshold that triggers a PING, the ACK deadline that declares the connection dead, and the timers tracking the last activity and any in-flight PING.
fn Keepalive::new( keepalive_time~ : Int64, keepalive_timeout~ : Int64, now? : Int64 = 0L) -> Keepalive
A keepalive that pings after keepalive_time of idleness and gives a PING keepalive_timeout to be acknowledged. now seeds the activity clock.
fn Keepalive::on_activity(self : Keepalive, now : Int64) -> Unit
Record connection activity (a frame sent or received) at now, resetting the idle timer.
fn Keepalive::should_ping(self : Keepalive, now : Int64) -> Bool
Whether a keepalive PING should be sent at now: the connection has been idle at least keepalive_time and no PING is already awaiting its ACK.
fn Keepalive::on_ping_sent(self : Keepalive, now : Int64) -> Unit
Record that a keepalive PING was sent at now.
fn Keepalive::on_ping_ack(self : Keepalive, now : Int64) -> Unit
Record that the PING's ACK arrived at now — the connection is alive, and this counts as activity.
fn Keepalive::is_timed_out(self : Keepalive, now : Int64) -> Bool
Whether the connection is dead at now: a PING is outstanding and its ACK has not arrived within keepalive_timeout of sending it.
§Load balancing and naming
Picking a backend and knowing whether it is reachable: the balancers, the connectivity state machine gRPC defines, DNS resolution, and the channel pool.
enum LbPolicy
A client-side load-balancing policy (← gRPC loadBalancingConfig).
struct LoadBalancer
A load balancer over a resolver's addresses, applying an LbPolicy to choose the next one.
fn LoadBalancer::new( addresses : Array[String], policy? : LbPolicy = PickFirst) -> LoadBalancer
A balancer over addresses (each a host:port) using policy (default PickFirst).
fn LoadBalancer::pick(self : LoadBalancer) -> String?
The address the next call should use, or None when the resolver produced none. PickFirst always returns the first address; RoundRobin returns each in turn, wrapping around.
fn LoadBalancer::size(self : LoadBalancer) -> Int
The number of addresses the balancer is choosing among.
enum ConnectivityState
A sub-connection's connectivity state (← gRPC connectivity.State).
struct SubConn
One address the Channel dials, tracking its connectivity state.
fn SubConn::new(address : String) -> SubConn
A fresh sub-connection to address, starting IDLE.
fn SubConn::connect(self : SubConn) -> Unit
Begin connecting (IDLE or TRANSIENT_FAILURE → CONNECTING). A no-op once SHUTDOWN or already connecting/ready.
fn SubConn::on_connected(self : SubConn) -> Unit
The connection attempt succeeded (CONNECTING → READY).
fn SubConn::on_failure(self : SubConn) -> Unit
A connection attempt or an established connection failed (→ TRANSIENT_FAILURE), unless shut down.
fn SubConn::on_idle(self : SubConn) -> Unit
A READY connection went idle (READY → IDLE).
fn SubConn::shutdown(self : SubConn) -> Unit
Shut the sub-connection down permanently (→ SHUTDOWN, a terminal state).
fn SubConn::is_ready(self : SubConn) -> Bool
Whether this sub-connection can carry RPCs right now.
let dns_type_a : Int = 1
The DNS record type for an IPv4 address.
let dns_type_aaaa : Int = 28
The DNS record type for an IPv6 address.
struct DnsRecord
One answer resource record, carrying the textual address for A/AAAA records (empty for other types).
struct DnsResponse
A decoded DNS response: the transaction id echoed back, the response code (0 = NOERROR), and the answer records.
fn dns_encode_query(id : Int, name : String, qtype : Int) -> Bytes
Encode a standard recursion-desired query for name of type qtype (dns_type_a or dns_type_aaaa) in class IN, with transaction id id.
fn dns_decode_response(msg : Bytes) -> DnsResponse
Decode a DNS response: echoed id, response code, and every answer record (A/AAAA records carry their textual address). Questions are skipped; authority and additional sections are ignored.
fn DnsResponse::addresses(self : DnsResponse) -> Array[String]
Every resolved A/AAAA address in the response, in record order.
struct ConnectionPool
A pool of sub-connections, one per resolved address (in resolver order), that picks a READY one per an LbPolicy.
fn ConnectionPool::new( addresses : Array[String], policy? : LbPolicy = PickFirst) -> ConnectionPool
A pool over addresses (each a host:port), each sub-connection starting IDLE, choosing among the READY ones with policy (default PickFirst).
fn ConnectionPool::subconn( self : ConnectionPool, address : String) -> SubConn?
The sub-connection for address, if the pool has one — the handle the Channel drives.
fn ConnectionPool::ready_addresses(self : ConnectionPool) -> Array[String]
The addresses of every READY sub-connection, in resolver order.
fn ConnectionPool::pick(self : ConnectionPool) -> String?
Pick a READY sub-connection's address for the next call, applying the LB policy over just the ready ones (← gRPC picker). None when no sub-connection is ready.
§Channels over a socket
The native client transports: a multiplexing channel that demultiplexes concurrent calls on one connection, a managed channel that dials several backends and routes around the dead ones, and the resolver behind it.
struct MuxChannel
A multiplexed channel over one connection: the engine keeps every call's stream state, inboxes wakes a waiting call when its stream is done, and write_lock serializes frame writes across concurrent callers.
async fn MuxChannel::connect(host : String, port : Int) -> MuxChannel
Open a multiplexed channel to host:port: connect and send the preface and client SETTINGS. Spawn read_pump into a task group to drive it before making calls.
async fn MuxChannel::read_pump(self : MuxChannel) -> Unit
The shared read loop: read a frame, feed it to the engine, write back any control response (SETTINGS ack, PING pong, WINDOW_UPDATE), and wake every call whose stream has completed. Returns when the connection closes. Spawn this once per channel.
async fn MuxChannel::unary( self : MuxChannel, path : String, request : Bytes, metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply
Make a unary call, concurrent with any other calls on this channel: register an inbox, send the request under the write lock, and wait for the pump to signal the stream done, then take the reply.
struct BidiStream
A bidirectional stream on a multiplexed channel: send request messages with send, half-close with close_send, and read reply messages one at a time with recv — all interleaved, sharing the connection with other calls. This is truly interactive streaming (send while receiving), which the one-shot Channel can't do.
fn MuxChannel::open_bidi( self : MuxChannel, path : String, metadata? : Array[@moonrpc.Header] = []) -> BidiStream
Open a bidirectional stream to path. Send request messages with send/close_send and read replies with recv.
async fn BidiStream::send(self : BidiStream, message : Bytes) -> Unit
Send one request message on the stream.
async fn BidiStream::close_send(self : BidiStream) -> Unit
Half-close the request side: no more messages will be sent.
async fn BidiStream::recv(self : BidiStream) -> Bytes?
Read the next reply message, blocking until one arrives. None once the server has finished the stream and every message has been read.
async fn MuxChannel::run_keepalive( self : MuxChannel, keepalive_time~ : Int64, keepalive_timeout~ : Int64) -> Unit
Drive keepalive on this channel: after keepalive_time ms of no traffic, send an HTTP/2 PING; if its ACK doesn't come back within keepalive_timeout ms the connection is declared dead and closed (← gRPC keepalive_time/keepalive_timeout). The read pump records incoming activity and PING ACKs. Spawn this alongside read_pump.
fn MuxChannel::close(self : MuxChannel) -> Unit
Close the connection. Outstanding calls are abandoned; the read pump exits.
struct ManagedChannel
A load-balanced channel over several backends. The pool holds one sub-connection per address (its connectivity state); channels holds the live Channel for each address that dialed successfully.
async fn ManagedChannel::connect( targets : Array[(String, Int)], policy? : @moonrpc.LbPolicy = PickFirst) -> ManagedChannel
Dial every backend in targets (each a host, port), marking each sub-connection READY on a successful connect and TRANSIENT_FAILURE on a dial error, then load-balance calls across the ones that came up. Addresses keep their given order, which is the order RoundRobin cycles and PickFirst prefers.
async fn ManagedChannel::dns_connect( dns_server : String, dns_port : Int, name : String, target_port : Int, policy? : @moonrpc.LbPolicy = PickFirst, id? : Int = 0x2a2a) -> ManagedChannel
Resolve name's A records via the DNS server at dns_server:dns_port, then dial each resolved address at target_port and load-balance across them — gRPC's dns:/// target end to end (name resolution feeding the connection pool).
fn ManagedChannel::ready_addresses(self : ManagedChannel) -> Array[String]
The addresses of every backend currently carrying calls, in resolver order — the READY subset the picker draws from.
async fn ManagedChannel::unary( self : ManagedChannel, path : String, request : Bytes, metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None, retry? : @moonrpc.RetryPolicy? = None) -> @moonrpc.CallReply
Make a unary call on a picked backend. Returns an UNAVAILABLE reply when no sub-connection is READY.
async fn ManagedChannel::unary_hedged( self : ManagedChannel, path : String, request : Bytes, hedging : @moonrpc.HedgingPolicy, metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply
Make a unary call under a hedging policy: fire attempts in parallel — a fresh one every delay_millis, each on a picked backend — and return the first whose status commits (OK or a fatal error), cancelling the rest. If every attempt finishes without committing (all non-fatal), the last reply is returned.
async fn ManagedChannel::server_streaming( self : ManagedChannel, path : String, request : Bytes, metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply
Make a server-streaming call on a picked backend.
async fn ManagedChannel::client_streaming( self : ManagedChannel, path : String, requests : Array[Bytes], metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply
Make a client-streaming call on a picked backend.
async fn ManagedChannel::bidi( self : ManagedChannel, path : String, requests : Array[Bytes], metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply
Make a bidirectional-streaming call on a picked backend.
fn ManagedChannel::close(self : ManagedChannel) -> Unit
Close every live backend connection.
async fn resolve_a( server : String, server_port : Int, name : String, id? : Int = 0x2a2a) -> Array[String]
Resolve name's A records by querying the DNS server at server:server_port over UDP, returning the dotted-decimal addresses (empty on a mismatched-id reply or an answer with no A records). id is the query's transaction id, echoed in the reply.
§Codecs
The gzip message compression gRPC negotiates and the base64 used by binary metadata, written here so the wire format needs no native binding.
suberror GzErrorA gzip / DEFLATE decode failure: a malformed member, an unsupported feature, or a checksum mismatch.
fn gunzip(data : Bytes) -> Bytes raise GzError
Decode a single gzip member (RFC 1952): validate the 10-byte header, skip the optional EXTRA / NAME / COMMENT / HCRC fields, inflate the DEFLATE body, and verify the trailing CRC-32 and ISIZE. Raises [GzError] on any malformation.
fn base64_encode(data : Bytes) -> Bytes
Standard base64 encode with = padding.
fn base64_decode(data : Bytes) -> Bytes
Standard base64 decode. Non-alphabet bytes (padding, whitespace) are skipped, so both padded and unpadded input decode.
fn is_binary_metadata(name : Bytes) -> Bool
Whether a metadata key names binary content: it ends in -bin (gRPC's convention for base64-on-the-wire values).
fn metadata_value_from_wire(name : Bytes, value : Bytes) -> Bytes
Decode a metadata value coming off the wire: base64-decoded for a -bin key, verbatim otherwise.
fn metadata_value_to_wire(name : Bytes, value : Bytes) -> Bytes
Encode a metadata value for the wire: base64-encoded for a -bin key, verbatim otherwise.