Errors & Decode Limits
Twilic decoders enforce safety limits on untrusted or malformed input. Understanding these limits is essential for production deployments at trust boundaries.
TwilicDecodeError (JavaScript)
class TwilicDecodeError extends Error {
readonly name = "TwilicDecodeError";
readonly code: "DECODE_DEPTH_EXCEEDED" | "DECODE_LIMIT_EXCEEDED";
}
const DEFAULT_MAX_DECODE_DEPTH = 64;
function decodeDepthLimitMessage(maxDepth: number): string;Thrown when decode() exceeds its nesting or collection/output budget. Errors outside these categories retain their original type.
Rust error types
pub enum TwilicError {
InvalidData(&'static str),
StatelessRetryRequired(&'static str, u64),
UnknownReference(&'static str, u64),
// ...
}Wire decode constants (Rust)
pub const DEFAULT_MAX_DECODE_COUNT: usize = 1 << 20;
pub const DEFAULT_MAX_DECODE_OUTPUT_RATIO: usize = 1 << 10;| Limit | Purpose |
|---|---|
| Decode depth | Prevents stack exhaustion from deeply nested arrays/maps |
| Decode count | Prevents allocation bombs from huge declared array/map lengths |
| Output ratio | Prevents decompression bombs (small input → huge output) |
Current source coverage
| Implementation/API | Limits added or extended |
|---|---|
Rust session decode_message / decode_value | Shared depth checks for nested values and messages; failed message decoding does not commit state |
| JavaScript fast and N-API decoders | Collection lengths, shape ID/key bounds and cumulative collection budget; normalized limit error codes |
| C/C++, Go, Python, Java/Kotlin/Scala, Ruby, PHP, Lua, R, Dart, Elixir | Reader length/count checks, nesting checks and cumulative vector/container budget on the implemented decoding paths |
| Swift | v2 depth/shape checks, checked wire lengths, vector budget; session protocol remains unsupported |
| C# | v2 depth/shape and reader bounds; no claim about unsupported vector/session APIs |
| Zig | Varuint termination, reader bounds, session recursion and vector counts |
Reader budgets use min(inputBytes * 1024, 1_048_576) accounting units. Collection slots and numeric values usually cost eight units; some paths also charge metadata or copied strings. Shape definitions are limited to IDs 0–65,535 and at most 256 keys where the v2 shape format is implemented. Nesting is bounded at 64 guarded frames; session message envelopes also consume depth, so this is not an identical number of array levels across APIs.
These are conservative accounting limits, not an exact heap-memory ceiling. Input buffers, strings, runtime object overhead, persistent session state and independent nested readers can consume additional memory. Cap input size and session lifetime separately. Some otherwise valid large or highly compressed messages that older versions accepted now fail; split them into smaller frames. The wire format is unchanged. Update/rebuild the relevant package and native/WASM artifacts to receive these changes.
Untrusted input guidelines
Twilic is designed for internal, trusted pipelines by default. At boundaries where bytes come from external users or compromised peers:
- Set maximum payload size before calling decode
- Never decode typeless or dynamically typed payloads from untrusted sources without an allowlist
- Keep public APIs on JSON or governed Protobuf
- Apply session state only on authenticated channels
- Monitor decode error rates — spikes may indicate attack or state drift
StatelessRetryRequired
When unknownReferencePolicy is statelessRetry and the decoder encounters an unknown base ID, shape reference, or dictionary ID:
TwilicError::StatelessRetryRequired("base_id", 777)The receiver should:
- Discard session state
- Request or wait for a full stateless frame
- Resume patching after baseline is re-established
Security at HTTP boundaries
Integration packages (@twilic/hono, etc.) decode request bodies. Treat this as a security boundary:
- Authenticate requests before decode
- Express and Hono parsers now default to a 1 MiB streaming body limit; set
limitexplicitly when required and keep reverse-proxy limits - Use
requireContentType: true(default) to reject unexpected media types
// Hono — reject non-Twilic Content-Type (default)
app.post("/data", twilicParser(), handler);
// Allow any Content-Type (use with caution)
app.post("/data", twilicParser({ requireContentType: false }), handler);Debugging decode failures
| Symptom | Likely cause |
|---|---|
DECODE_DEPTH_EXCEEDED | Deeply nested or malicious payload |
decode count limit exceeded | Corrupt length prefix or attack |
StatelessRetryRequired | Client/server state drift — call reset() |
failed to decode payload | Wrong format version or truncated bytes |
| HTTP 413 | Body exceeds the configured byte limit |
| HTTP 415 | Missing or wrong Content-Type header |
Use the Twilic CLI to inspect bytes:
cat payload.twilic | twilic decode --prettyOr decode to transport JSON (JavaScript advanced):
import { decodeToTransportJson } from "@twilic/core/advanced";
console.log(decodeToTransportJson(bytes));