Middleware & Reliability
Every channel accepts an optional ChannelOptions table controlling how a packet is delivered and what happens to its data before your callback runs.
type ChannelOptions = {
reliability : ReliabilityType?,
middleware : {MiddlewareFn}?,
}| Field | Type | Default | Description |
|---|---|---|---|
reliability | ReliabilityType | "reliable" | Delivery guarantee, forwarded to the underlying ByteNet packet's reliabilityType. |
middleware | {MiddlewareFn} | {} | Ordered list of gatekeeper functions run on every incoming on() callback. |
Reliability
type ReliabilityType = "reliable" | "unreliable" | "unreliableOrdered"| Value | Guarantees delivery? | Guarantees order? | Typical use case |
|---|---|---|---|
reliable | Yes | Yes | Damage events, inventory changes, chat — anything that must never be dropped. |
unreliable | No | No | High-frequency data where the newest value matters more than every value, e.g. per-frame position updates. |
unreliableOrdered | ⚠️ Not actually supported | ⚠️ Not actually supported | See warning below. |
Known limitation: the underlying ByteNet 0.4.6 packet type only
recognizes reliabilityType: ("reliable" | "unreliable")? — there is no
third option. KravasNet's ReliabilityType still declares
"unreliableOrdered" for forward-compatibility, but passing it today
will not behave as "ordered unreliable" delivery — at best ByteNet
falls back to whatever its reliabilityType == "reliable" check
resolves to (i.e. it silently behaves as "unreliable", since anything
that isn't exactly "reliable" takes the unreliable code path). Do not
rely on "unreliableOrdered" for anything that needs in-order delivery
until ByteNet itself adds real support for it. Stick to "reliable" or
"unreliable".
CombatNet:add("Position", { pos = KravasNet.vec3 }, { reliability = "unreliable" })If reliability is omitted, KravasNet defaults to "reliable", matching
ByteNet's own safest default.
Middleware
type MiddlewareFn = (data: any, player: Player?) -> (shouldContinue: boolean, data: any?)Middleware functions are gatekeepers attached to a channel's on() listener. They run in the order supplied, once per received packet, before your callback executes.
| Return value | Meaning |
|---|---|
false (first value) | Packet is dropped. The user callback is never invoked for this packet. |
true (first value) | Packet continues to the next middleware (or to the callback if it was the last one). |
second value ~= nil | Replaces data for the remainder of the chain and for the final callback. |
second value == nil | data is left unchanged. |
Execution flow
local function runMiddleware(fns, data, player)
for _, fn in ipairs(fns) do
local ok, result = fn(data, player)
if not ok then
return false, nil
end
if result ~= nil then
data = result
end
end
return true, data
endThis is the exact internal implementation: middleware is short-circuiting — the first function that returns false stops the chain immediately, and the callback is skipped entirely for that packet.
Example: server-side validation
local function clampDamage(data, player)
if type(data.amount) ~= "number" then
return false -- drop malformed packets
end
data.amount = math.clamp(data.amount, 0, 100)
return true, data
end
CombatNet:add("Damage", { amount = KravasNet.float32 }, {
middleware = { clampDamage },
})Example: chaining multiple middlewares
local function mustBeAlive(data, player)
return player.Character ~= nil and player.Character:FindFirstChild("Humanoid") ~= nil
end
local function logPacket(data, player)
print(`[Combat] {player.Name} sent`, data)
return true -- data unchanged
end
CombatNet:add("Damage", { amount = KravasNet.float32 }, {
middleware = { mustBeAlive, logPacket, clampDamage },
})Middleware runs on both server and client on() calls, since
runMiddleware is shared code. On the client, the player argument
passed into your middleware functions will be nil — the client-side
on callback signature only forwards data.
Next: see the full list of supported Data Types, or jump to Best Practices.