XTLS/Xray-core · error
prefix length %d leaves no padding record
Error message
prefix length %d leaves no padding record
What it means
After the prefix-consumption loop, either some prefix bytes remain unconsumed (remainingPrefix != 0) or every chunk of the variant was eaten (firstChunk == len(variant.chunks)), leaving zero padding records for the turn. The reader expects at least one record after the prefix, so the schedule is rejected. This is the boundary case of the prefix/chunk alignment contract enforced by trimPaddingPrefix.
Source
Thrown at transport/internet/finalmask/xmc/padding.go:300
if err != nil {
return 0, nil, nil, err
}
return targetLength, chunks, delays, nil
}
func trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {
remainingPrefix := prefixLength
firstChunk := 0
for firstChunk < len(variant.chunks) && remainingPrefix > 0 {
chunkLength := variant.chunks[firstChunk]
if remainingPrefix < chunkLength {
return nil, nil, fmt.Errorf("prefix length %d splits chunk %d", prefixLength, firstChunk)
}
remainingPrefix -= chunkLength
firstChunk++
}
if remainingPrefix != 0 || firstChunk == len(variant.chunks) {
return nil, nil, fmt.Errorf("prefix length %d leaves no padding record", prefixLength)
}
chunks := append([]int(nil), variant.chunks[firstChunk:]...)
delays := make([]paddingDelayRange, len(chunks))
if len(variant.delays) > 0 {
copy(delays, variant.delays[firstChunk:])
}
return chunks, delays, nil
}
func defaultPaddingChunks(recordLength, writeChunkLength int) []int {
chunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)
for remaining := recordLength; remaining > 0; {
chunkLength := min(remaining, writeChunkLength)
chunks = append(chunks, chunkLength)
remaining -= chunkLength
}
return chunksView on GitHub (pinned to 7d214f8b09)
Solutions
- Add at least one extra chunk after the prefix boundary to every variant (increase total variant length beyond the prefix).
- Reduce firstTurnPrefixLength so at least one chunk survives after consumption.
- Use the shipped presets, which guarantee trailing records after the prefix.
- Assert in tests that selectPaddingVariant(variant, prefix) returns at least one chunk for every variant in turn one.
Example fix
// before: prefix 4096 consumes the only chunk, no record remains
chunks: []int{4096}
// after: one record survives after the prefix
chunks: []int{4096, 512} Defensive patterns
Strategy: validation
Validate before calling
func variantSurvivesPrefix(prefix int, chunks []int) bool {
sum := 0
for _, c := range chunks {
sum += c
}
return prefix < sum && prefix >= chunks[0] // at least one chunk or partial remainder left
} Prevention
- Ensure every first-turn variant totals strictly more than the prefix length.
- Test selectPaddingVariant(variant, prefix) returns len(chunks) >= 1 for all variants.
- Re-run preset validation tests after changing the negotiated prefix.
When it happens
Trigger: A firstTurnPrefixLength exactly equal to (or greater than) the total length of every variant in the turn, so the prefix consumes all chunks; or a prefix that is longer than the variant total leaving leftover bytes no chunk can absorb.
Common situations: Configuring a first turn whose variants total exactly the prefix size (e.g. prefix 4096 with a single 4096-byte chunk); mixing presets built for a smaller prefix with a peer that now sends a larger prefix after a version change.
Related errors
- prefix length %d splits chunk %d
- invalid range: %d-%d
- variants cannot be combined with a length range
- variant %d has no chunks
- metrics must have a tag or listen address
AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15).
Data as JSON: /api/errors/b1fbc623a1d675ee.
Report an issue: GitHub.