What Is Fragmentation and Why We Care About It?
Imagine we have a stream of subtitle from a 10 minute video. Subtitle
is being render on client side in real time as video
playing through WebSocket connection. How the whole
subtitle sent successfuly to client?
If we answer just send whole subtitle, the answer is
less precise. Let's step back to previous article where
we discusses about payload length.
RFC states maximum payload length in single frame.
WebSocket RFC limits message payload in a single
frame, where it capped at 2^63 bytes. Consequently,
a frame with it's message payload exceeds 2^63 bytes
is non-compliant. To make it compliant,
that frame should comply fragmentation.
Fragmentation is a protocol to split complete WebSocket
message into multiple frames. For example, consider
a complete text message abcdefgh. We can send
that message as one complete text frame or multiple
text frames by apply fragmentation protocol. Number of
text frames is arbitrary in count, not in structure
during fragmentation. A compliant server must implements fragmentation rules correctly from
receives to assembles complete message, leaving no
room for protocol violation.
One valid fragmented sequence of abcdefgh looks like this:
Frame 1:
FIN = 0
Opcode = 1 (text)
Payload = "abc"
Frame 2:
FIN = 0
Opcode = 0 (continuation)
Payload = "def"
Frame 3:
FIN = 1
Opcode = 0 (continuation)
Payload = "gh"
Why first frame's FIN is 0 and last frame's FIN is 1?
Why other frame's opcode is 0 except first frame? What
about control frames?. We'll answer these questions
in next section.
Fragmentation Rules
This article focuses on direct client-server WebSocket
fragmentation rules as defined by RFC 6455 without
negotiated extensions or intermediaries (such as proxies
or message-transforming middlewares).
While RFC also defines considerations for extensions and
intermediaries, those topics are outside of this article.
The rules presented below apply to a baseline, RFC-compliant
WebSocket endpoint.
- An unfragmented/complete message consists of a single frame with FIN bit set (FIN = 1) and an opcode other than 0
- A fragmented message consists of single frame with FIN bit clear (FIN = 0) and an opcode other than 0, followed by zero or more frames with FIN bit clear and opcode set to 0, and terminated by single frame with FIN bit set and an opcode of 0
- Control frames MAY be injected in the middle of fragmented message. Control frames themselves MUST NOT be fragmented
- Message fragments MUST be delivered to the recipient in the order sent by the sender.
- An endpoint MUST be capable of handling control frames in the middle of fragmented message.
- A sender MAY create fragments of any size for non-control messages.
As a consequence of these rules, abcdefgh message may
be arranged as:
Case 1: 4 Frames without injected control frame during
fragmentation
Frame A:
FIN = 0
opcode = 1
message = `ab`
Frame B:
FIN = 0
opcode = 0
message = `cd`
Frame C:
FIN = 0
opcode = 0
message = `ef`
Frame D:
FIN = 1
opcode = 0
message = `gh`
This is valid fragmentation. Reciever/server begins buffering
message when it receives frame A, continues buffering frame B
and C, then assembles complete message upon receives frame D.
Case 2: 4 Frames without injected control frame, but middle
of fragmented message has invalid opcode
Frame A:
FIN = 0
opcode = 1
message = `ab`
Frame B:
FIN = 0
opcode = 1
message = `cd`
Frame C:
FIN = 0
opcode = 0
message = `ef`
Frame D:
FIN = 1
opcode = 0
message = `gh`
As consequence of rule #2, this sequence mark as invalid
fragmentation, because to opcode of frame B is not 0
(violates rule #2).
Case 3: 5 Frames with appearance of consecutive control frame
during fragmentation
Frame A:
FIN = 0
opcode = 1
message = `ab`
Frame B:
FIN = 1
opcode = 9
message = `X`
Frame C:
FIN = 1
opcode = 9
message = `X`
Frame D:
FIN = 0
opcode = 0
message = `cdef`
Frame E:
FIN = 1
opcode = 0
message = `gh`
This sequence is valid fragmentation. A compliant server must
begin buffering upon receives frame A, then respond to ping
message when receives frame B and C immediately (as required
by rule #3 and #4).
Server must not treat these control frames as part of whole
message and must not buffer them. Server continues to buffering
message upon receives frame D, and finalize whole message
when receives frame E (Rule #5).
Case 4: 4 Frames with opcode of middle fragmented frame is not
0
Frame A:
FIN = 0
opcode = 1
message = `ab`
Frame B:
FIN = 1
opcode = 1
message = `cd`
Frame C:
FIN = 0
opcode = 0
message = `ef`
Frame D:
FIN = 1
opcode = 0
message = `gh`
This case mark as invalid fragmentation, because opcode of
frame B is not 0 during fragmentation (violates rule #2).
The server must terminates connection immediately with protocol
error code upon receiving frame B before processing frame
C and D.
Case 5: 3 frames with opcode of final frame is not 0
Frame A:
FIN = 0
opcode = 1
message = `ab`
Frame B:
FIN = 0
opcode = 0
message = `cd`
Frame C:
FIN = 1
opcode = 1
message = `efgh`
This is invalid fragmentation sequence, because opcode of
final frame (frame C) is not set to 0 (violates rule #2)
Earlier on this article, we stated that number of frames
in fragmentation is arbitrary in count, not in structure.
Arbitrary in count demonstrated by case 1, where we
split whole message to different number of frames. Client
can choose number of frames freely as long as the fragmentation
rules are followed
Not arbitrary in structure demonstrated by case 2, 4, and 5
where those cases violates fragmentation rules - such as
using a non-zero opcode in the middle of fragmented message
or terminating fragmentation using non-continuation opcode.
These sequences are invalid, and resulting protocol error.
Given these rules, a compliant WebSocket endpoint effectively
operates as a small state machine while receiving frames.
state = IDLE
buffer = empty
on recieving frame:
if frame is control frame:
handle control frame immediately
continue to next frame
if state == IDLE:
if FIN == 1 and opcode != 0:
deliver message directly
else if FIN == 0 and opcode != 0:
state = FRAGMENTING
start buffering
else:
return protocol error
else if state == FRAGMENTING:
if opcode != 0:
return protocol error
append message to buffer
if FIN == 1:
deliver reassembled message
clear buffer
state = IDLE
We'll refine this state machine to pseudocode along with
Golang implementation on next section.
Implementing Fragmentation
Fragmentation is a connection level state instead of frame level
logic. As shown by state machine on previous section, a fragmentation
compliant WebSocket endpoint must maintain connection scoped context
in order to process incoming frames correctly.
Specifically, such an endpoint must capable of:
- Survive control frame interruptions
Control frame may appear in the middle of fragmentation. Endpoints
must be able to respond incoming control frame immediately without
breaking or resetting ongoing fragmentation process
- Remember previous frames
Fragmentation rules expects endpoints to accept or reject frame
not only based on frame currently being processed, but also
prior frame. If a frame violates fragmentation rules given the
prior context, endpoint must close connection with protocol error.
In other words, a validity of WebSocket fragmentation is determined
by sequence of frames that being processes on same connection, instead
only verify single frame.
Let's translate state machine from previous section to pseudocode
as our blueprint, before jump to Go implementation.
if frame.opcode is control frame{
handleControlFrame(frame)
continue
}
if FIN == 0{
if state == IDLE{
if first frame opcode == 0{
return protocol error
}
beginFragmentation(frame)
state = FRAGMENTING
initialOpcode = opcode
continue read next frame
}
if frame.opcode != 0{
return protocol error
}
bufferFragment(frame)
continue read next frame
}else{
if state == FRAGMENTING{
if frame.opcode != 0{
return protocol error
}
assembleFragment(frame)
state = IDLE
continue read next frame
}
if frame.opcode == 0{
return protocol error
}
echo(frame)
}
Based on pseudocode above, fragmentation handled upon recieving
frame. Looking back on our previous code, handleRequest is
responsible to that action. Let's enhance previous code to handle
fragmentation and RFC compliant.
type WebSocketStatusCode uint16
var (
StatusNormalClosure WebSocketStatusCode = 1000
StatusProtocolError WebSocketStatusCode = 1002
StatusMessageTooBig WebSocketStatusCode = 1009
)
type ws struct {
conn net.Conn
reader *bufio.Reader
writer *bufio.Writer
assemblingRequest bool
initialBufferOpcode uint8
bufferedRequest []byte
}
type frame struct {
opcode uint8
payloadIndicator int64
actualPayloadLength int64
bigEndianPayloadFormat []byte
isFin bool
isMasked bool
}
func (ws *ws) handleRequest() error {
defer ws.conn.Close()
for {
frame, err := ws.readRequest(2)
if err != nil {
return err
}
if isControlFrame(frame.opcode) {
if !frame.isFin || frame.actualPayloadLength > 125 {
return ws.closeWithCode(frame, StatusProtocolError)
}
switch frame.opcode {
case 8:
return ws.closeWithCode(frame, StatusNormalClosure)
case 9:
err = ws.pong(frame)
if err != nil {
return err
}
continue
}
}
if !frame.isFin {
if !ws.assemblingRequest {
if frame.opcode == 0 {
return ws.closeWithCode(frame, StatusProtocolError)
}
ws.assemblingRequest = true
ws.initialBufferOpcode = frame.opcode
err = ws.bufferRequest(frame)
if err != nil {
return err
}
continue
}
if frame.opcode != 0 {
return ws.closeWithCode(frame, StatusProtocolError)
}
err = ws.bufferRequest(frame)
if err != nil {
return err
}
continue
} else {
if ws.assemblingRequest {
if frame.opcode != 0 {
return ws.closeWithCode(frame, StatusProtocolError)
}
err = ws.assembleRequest(frame)
if err != nil {
return err
}
continue
}
if frame.opcode == 0 {
return ws.closeWithCode(frame, StatusProtocolError)
}
err = ws.echo(frame)
if err != nil {
return err
}
}
}
}
func (ws *ws) assembleRequest(frame frame) error {
switch ws.initialBufferOpcode {
case 1:
return ws.writeTextFrameResponse(frame)
case 2:
return ws.writeBinaryFrameResponse(frame)
default:
return ws.closeWithCode(frame, StatusProtocolError)
}
}
func (ws *ws) closeWithCode(_ frame, closeCode WebSocketStatusCode) error {
uCloseCode := uint16(closeCode)
bufCloseCode := make([]byte, 2)
binary.BigEndian.PutUint16(bufCloseCode, uCloseCode)
responseFrame := []byte{
0x88, // FIN + Close opcode
0x02, // payload length = 2
bufCloseCode[0],
bufCloseCode[1],
}
_, err := ws.writer.Write(responseFrame)
if err != nil {
return err
}
err = ws.writer.Flush()
if err != nil {
return err
}
return ws.conn.Close()
}
Fragmentation state handled in connection instead of frame.
WebSocket connection information and fragmentation state are store in ws struct. We enhance
the struct by adding assemblingRequest field to track fragmentation
state, initialBufferOpcode to save opcode of first frame
during fragmentation, which later server will determines final assembled
message is text or binary frame, and bufferedRequest to store valid
fragmented frames.
We've been demonstrated valid and invalid fragmentation sequences.
When server encounters invalid fragmented frame during fragmentation,
server close connection with specific error code. closeWithCode
terminates connection between server and client with specific close code.
Close code and its reason defined at section 7.4.1.
Code Refinement Spotlight
package main
import (
"bufio"
"crypto/sha1"
"encoding/binary"
"errors"
"io"
"log"
"net"
"strings"
b64 "encoding/base64"
)
const (
secWsKey = "Sec-WebSocket-Key"
connHeaderKey = "Connection"
connHeaderVal = "Upgrade"
upgradeHeaderKey = "Upgrade"
upgradeConnHeaderVal = "websocket"
)
const (
MAX_PAYLOAD_LENGTH = 262144
)
type WebSocketStatusCode uint16
var (
StatusNormalClosure WebSocketStatusCode = 1000
StatusProtocolError WebSocketStatusCode = 1002
StatusMessageTooBig WebSocketStatusCode = 1009
)
var (
ErrUnmaskedFrame = errors.New("client frame must be masked")
ErrPayloadTooBig = errors.New("payload too big")
)
type ws struct {
conn net.Conn
reader *bufio.Reader
writer *bufio.Writer
assemblingRequest bool
initialBufferOpcode uint8
bufferedRequest []byte
}
type frame struct {
opcode uint8
payloadIndicator int64
actualPayloadLength int64
bigEndianPayloadFormat []byte
isFin bool
isMasked bool
}
func main() {
port := ":8083"
listener, err := net.Listen("tcp", port)
if err != nil {
log.Fatal(err)
}
log.Println("running on ", port)
for {
conn, err := listener.Accept()
if err != nil {
log.Fatalln(err)
}
go handshake(conn)
}
}
func handshake(conn net.Conn) error {
var secWsAccept string
reader := bufio.NewReader(conn)
for {
header, err := reader.ReadString('\n')
if err != nil && !errors.Is(err, io.EOF) {
return err
}
if header == "\r\n" || header == "\n" {
break
}
secWsAcceptVal, err := readHTTPUpgradeHeaderRequest(header)
if err != nil {
return err
}
if secWsAcceptVal != "" {
secWsAccept = secWsAcceptVal
}
}
writer := bufio.NewWriter(conn)
upgradeResp := []string{
"HTTP/1.1 101 Web Socket Protocol Handshake",
"Server: go/echoserver",
"Upgrade: WebSocket",
"Connection: Upgrade",
"Sec-WebSocket-Accept: " + secWsAccept,
"", // required for extra CRLF
"", // required for extra CRLF
}
_, err := writer.Write([]byte(strings.Join(upgradeResp, "\r\n")))
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
ws := ws{
conn: conn,
reader: reader,
writer: writer,
}
return ws.handleRequest()
}
// read HTTP upgrade request, returns Sec-WebSocket-Accept header
// value if header is Sec-WebSocket-Accept. Otherwise, checking
// other upgrade header defined at https://datatracker.ietf.org/doc/html/rfc6455#autoid-4
func readHTTPUpgradeHeaderRequest(header string) (string, error) {
var secWsAccept string
headerKeys := strings.Split(header, ":")
headerKey := strings.TrimSpace(headerKeys[0])
switch {
case headerKey == upgradeHeaderKey:
uUpgradeVal := strings.TrimSpace(headerKeys[1])
if uUpgradeVal != upgradeConnHeaderVal {
return "", errors.New("upgrade header value is not websocket")
}
return "", nil
case strings.Contains(header, connHeaderKey):
cConnVal := strings.TrimSpace(headerKeys[1])
if cConnVal != connHeaderVal {
return "", errors.New("conenection header value is not upgrade")
}
return "", nil
case strings.Contains(header, secWsKey):
sSecWsVal := strings.TrimSpace(headerKeys[1])
secWsAccept = sSecWsVal + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
sha := sha1.New()
sha.Write([]byte(secWsAccept))
encSecWsAccept := sha.Sum(nil)
secWsAccept = b64.StdEncoding.EncodeToString(encSecWsAccept)
return secWsAccept, nil
}
return "", nil
}
func (ws *ws) handleRequest() error {
defer ws.conn.Close()
for {
frame, err := ws.readRequest(2)
if err != nil {
return err
}
// beginning of fragmentation
if isControlFrame(frame.opcode) {
if !frame.isFin || frame.actualPayloadLength > 125 {
return ws.closeWithCode(frame, StatusProtocolError)
}
switch frame.opcode {
case 8:
return ws.closeWithCode(frame, StatusNormalClosure)
case 9:
err = ws.pong(frame)
if err != nil {
return err
}
continue
}
}
if !frame.isFin {
if !ws.assemblingRequest {
if frame.opcode == 0 {
return ws.closeWithCode(frame, StatusProtocolError)
}
ws.assemblingRequest = true
ws.initialBufferOpcode = frame.opcode
err = ws.bufferRequest(frame)
if err != nil {
return err
}
continue
}
if frame.opcode != 0 {
return ws.closeWithCode(frame, StatusProtocolError)
}
err = ws.bufferRequest(frame)
if err != nil {
return err
}
continue
} else {
if ws.assemblingRequest {
if frame.opcode != 0 {
return ws.closeWithCode(frame, StatusProtocolError)
}
err = ws.assembleRequest(frame)
if err != nil {
return err
}
continue
}
if frame.opcode == 0 {
return ws.closeWithCode(frame, StatusProtocolError)
}
err = ws.echo(frame)
if err != nil {
return err
}
}
}
}
func (ws *ws) echo(frame frame) error {
switch frame.opcode {
case 1:
return ws.writeTextFrameResponse(frame)
case 2:
return ws.writeBinaryFrameResponse(frame)
case 8:
return ws.closeWithCode(frame, StatusNormalClosure)
case 9:
return ws.pong(frame)
case 10:
}
return nil
}
func (ws *ws) assembleRequest(frame frame) error {
switch ws.initialBufferOpcode {
case 1:
return ws.writeTextFrameResponse(frame)
case 2:
return ws.writeBinaryFrameResponse(frame)
default:
return ws.closeWithCode(frame, StatusProtocolError)
}
}
func (ws *ws) writeBinaryFrameResponse(_ frame) error {
return nil
}
func (ws *ws) resetFragmentation() {
ws.assemblingRequest = false
ws.bufferedRequest = []byte{}
ws.initialBufferOpcode = 0
}
func isControlFrame(opcode uint8) bool {
return opcode == 0x8 || opcode == 0x9 || opcode == 0xA
}
func (ws *ws) bufferRequest(frame frame) error {
requestPayload, err := ws.readPayload(frame)
if err != nil {
return err
}
bufferAvailable := len(ws.bufferedRequest)+int(len(requestPayload)) <= MAX_PAYLOAD_LENGTH
if !bufferAvailable {
return ws.closeWithCode(frame, StatusMessageTooBig)
}
ws.bufferedRequest = append(ws.bufferedRequest, requestPayload...)
ws.assemblingRequest = true
return nil
}
func (ws *ws) readRequest(requestSize int) (frame, error) {
header := make([]byte, requestSize)
_, err := io.ReadFull(ws.reader, header)
if err != nil {
return frame{}, err
}
fin := (header[0] & 0x80) != 0
opcode := header[0] & 0xf
payloadLengthIndicator := header[1] & 0x7f
isMasked := header[1]&0x80 != 0
if !isMasked {
return frame{}, ErrUnmaskedFrame
}
var actualPayloadLength uint64
frame := frame{}
switch {
case payloadLengthIndicator < 126:
actualPayloadLength = uint64(payloadLengthIndicator)
case payloadLengthIndicator == 126:
actualPayloadByte := make([]byte, 2)
_, err = io.ReadFull(ws.reader, actualPayloadByte)
if err != nil {
return frame, err
}
actualPayloadLength = uint64(binary.BigEndian.Uint16(actualPayloadByte))
frame.bigEndianPayloadFormat = actualPayloadByte
case payloadLengthIndicator == 127:
actualPayloadByte := make([]byte, 8)
_, err = io.ReadFull(ws.reader, actualPayloadByte)
if err != nil {
return frame, err
}
actualPayloadLength = uint64(binary.BigEndian.Uint64(actualPayloadByte))
if actualPayloadLength > MAX_PAYLOAD_LENGTH {
return frame, errors.New("payload too big")
}
frame.bigEndianPayloadFormat = actualPayloadByte
}
frame.isFin = fin
frame.opcode = opcode
frame.isMasked = isMasked
frame.payloadIndicator = int64(payloadLengthIndicator)
frame.actualPayloadLength = int64(actualPayloadLength)
return frame, nil
}
func (ws *ws) writeTextFrameResponse(frame frame) error {
responseByte, err := ws.readPayload(frame)
if err != nil {
return err
}
if len(ws.bufferedRequest) != 0 {
asembledResponse := []byte(ws.bufferedRequest)
asembledResponse = append(asembledResponse, responseByte...)
if len(responseByte) >= MAX_PAYLOAD_LENGTH {
return ws.closeWithCode(frame, StatusMessageTooBig)
}
responseByte = asembledResponse
}
outOpcode := frame.opcode
if ws.assemblingRequest {
outOpcode = ws.initialBufferOpcode
}
if len(responseByte) <= 125 {
responseFrame := make([]byte, 2)
responseFrame[0] = 0x80 | outOpcode
responseFrame[1] = byte(len(responseByte))
responseFrame = append(responseFrame, responseByte...)
_, err = ws.writer.Write(responseFrame)
if err != nil {
return err
}
}
if len(responseByte) >= 126 && len(responseByte) <= 65535 {
responseFrame := make([]byte, 2)
responseFrame[0] = 0x80 | outOpcode
responseFrame[1] = 0x7e
actualPayloadFrameByte := make([]byte, 2)
binary.BigEndian.PutUint16(actualPayloadFrameByte, uint16(len(responseByte)))
responseFrame = append(responseFrame, actualPayloadFrameByte...)
responseFrame = append(responseFrame, responseByte...)
_, err = ws.writer.Write(responseFrame)
if err != nil {
return err
}
}
if len(responseByte) >= 65536 {
responseFrame := make([]byte, 2)
responseFrame[0] = 0x80 | outOpcode
responseFrame[1] = 0x7f
actualPayloadFrameByte := make([]byte, 8)
binary.BigEndian.PutUint64(actualPayloadFrameByte, uint64(len(responseByte)))
responseFrame = append(responseFrame, actualPayloadFrameByte...)
responseFrame = append(responseFrame, responseByte...)
_, err = ws.writer.Write(responseFrame)
if err != nil {
return err
}
}
err = ws.writer.Flush()
if err != nil {
return err
}
return nil
}
func (ws *ws) closeWithCode(_ frame, closeCode WebSocketStatusCode) error {
uCloseCode := uint16(closeCode)
bufCloseCode := make([]byte, 2)
binary.BigEndian.PutUint16(bufCloseCode, uCloseCode)
responseFrame := []byte{
0x88, // FIN + Close opcode
0x02, // payload length = 2
bufCloseCode[0],
bufCloseCode[1],
}
_, err := ws.writer.Write(responseFrame)
if err != nil {
return err
}
err = ws.writer.Flush()
if err != nil {
return err
}
return ws.conn.Close()
}
func (ws *ws) pong(frame frame) error {
responseData, err := ws.readPayload(frame)
if err != nil {
return err
}
responseFrame := make([]byte, 2+len(responseData))
responseFrame[0] = 0x8a
responseFrame[1] = byte(len(responseData))
copy(responseFrame[2:], responseData)
_, err = ws.writer.Write(responseFrame)
if err != nil {
return err
}
return ws.writer.Flush()
}
func (ws *ws) readPayload(frame frame) ([]byte, error) {
if !frame.isMasked {
return nil, ErrUnmaskedFrame
}
maskingKey := make([]byte, 4)
_, err := io.ReadFull(ws.reader, maskingKey)
if err != nil {
return nil, err
}
validPayloadLength := frame.actualPayloadLength >= 0 && frame.actualPayloadLength <= int64(MAX_PAYLOAD_LENGTH)
if !validPayloadLength {
return nil, ErrPayloadTooBig
}
n := int(frame.actualPayloadLength)
requestData := make([]byte, n)
_, err = io.ReadFull(ws.reader, requestData)
if err != nil {
return nil, err
}
for i := 0; i < len(requestData); i++ {
requestData[i] = requestData[i] ^ maskingKey[i%4]
}
return requestData, nil
}
We will breakdown code refinements on following subsections
Fragmentation State Tracking
Before Refinement:
Treats frame individually, without knowing prior frame on same
connection.
After Refinement:
Server stores previous frame information if client's request contains fragmentation state.
Why It Matters:
Fragmentation is a protocol mechanism that forces an endpoint to
retain state accross multiple frames. It cannot be enforced by handling
single frame in isolation. Validity of fragmentation state depends on earlier frame on the same connection.
For example:
An opcode 0x0 is invalid unless endpoint enters fragmentation
An opcode 0x1 or 0x2 is invalid if endpoint receives in the
middle of fragmentation
Without connection-scoped validation, an endpoint cannot determine
whether incoming request is valid continuation request or
violates fragmentation rules.
Changes On This Article:
Refined handleRequest() to handle fragmentation rules defined by RFC. Additionally, introduces isFin field at frame struct to detect FIN bit. Additionally, added assemblingRequest, initialBufferOpcode, and bufferedRequest onws` struct to track fragmentation state.
Buffer Fragmented Frames
Before Refinement:
Echoes request immediately without retaining prior frame information.
After Refinement:
Buffer request message if connection enters fragmentation state
Why It Matters:
Endpoint stores/buffers incoming frames until final frame (FIN=1)
arrives. Before it arrives, endpoint must not:
- Deliver partial messages to client.
- Interprets buffered text/binary messages.
- Apply application-level logic.
Buffering ensures that:
- Message boundaries are preserved
- UTF-8 validation for text frame can be applied to complete message.
- Oversized messages can be rejected before final message assembly.
Changes On This Article:
Introduces ws.bufferRequest function to buffer request message
if connection enters fragmentation state.
Protocol Violation Handling
Before Refinement:
Previous code only return 1000 status code (normal closure) by default,
ignores status code related to protocol violation. Fragmentation
rules require more specific status code including protocol error
or message too large that endpoint unable to handle.
After Refinement:
Uses defined status code related to protocol
violation such as protocol error or message too big
Why It Matters:
RFC 6455 defines protocol violations as connection-fatal errors. This
means, whenever fragmentation rules are violated, endpoint must terminates
connection with client.
Continuing fragmentation after violation risks:
- Whole message misinterpretation.
- Accepting attacker-controlled frame sequence.
- Desynchronizing frame boundaries.
Changes On This Article:
Replaces ws.close() with ws.closeWithCode(),
which closes WebSocket connection between client and
server. Additionally, the function sends
RFC defined status code as connection termination reason
Control Frame Interleaving
Before Refinement:
Control frame treated like normal data or simply ignored
After Refinement:
Control frame handled immediately and never affected fragmentation
state
Why It Matters:
Control frames are allowed to appear at any point during fragmentation.
They are not part of message and must not taking part of
message assembling.
Endpoint must handle them immediately at the time when control frame appears,
and must not treat control frame as part of client's message
Changes On This Article:
Added isControlFrame() to detect whether incoming frame is control
frame or not. It called before fragmentation rules applied.
Contextual Frame Validity
Before Refinement:
Handles single frame immediately and ignores previous frame
After Refinement:
Preserves prior frame state including first frame opcode
and fragmentation state tracking.
Why It Matters:
As a consequence of fragmentation rules, endpoint must retain previous
frame state to determine whether current frame is valid. For example:
- An continuation opcode
0x0is invalid if no fragmentation is active on the connection. - A text frame or binary frame is invalid if fragmentation is active.
- A fragmented control frame is always invalid
Changes On This Article:
Added assemblingRequest to track fragmentation state and initialBufferOpcode
to preserve opcode of first frame.
Closing Thought
RFC states maximum payload length of a single frame at 2^63 bytes.
A server that complies RFC must be able to handle frame that exceeds
the maximum payload length. To answer this limitation, a server must
follow fragmentation protocol.
Fragmentation is a protocol that splits WebSocket message into multiple
frames. The frames are abritrary in number, but not in structure. In other
words, we can split single frame to 3, 4, or 5 frames, but their order
must not be change by applying opcode rules and preserving continuation semantics.
Fragmentation is connection level instead of frame level logic. This means
that a compliant server must be able to tracks and checks prior frame to
determine whether incoming request is valid fragmentation.
At the end of this article, we've been enhanced our previous code
by not only handles extended payload length, but also tracks
fragmentation state on connection level.
Top comments (0)