Skip to content

Return short-write errors from frame writers - #232

Merged
gobwas merged 2 commits into
gobwas:masterfrom
omrzk:fix/frame-short-writes-150
Oct 26, 2026
Merged

gobwas merged 2 commits into
gobwas:masterfrom
omrzk:fix/frame-short-writes-150

Conversation

@omrzk

@omrzk omrzk commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #150.

Frame writes currently report success when an io.Writer accepts only part of the header or payload without returning an error. Return io.ErrShortWrite in those cases, including WriteHeaderBuffer, and stop before sending the payload if its header was truncated. Explicit writer errors are preserved.

Regression fixtures fail on master for short headers and payloads. go test -race -shuffle=on -cover ./..., go vet ., go build ./..., go mod verify, gofmt and git diff --check pass on Go 1.26.10. Full-package go vet ./... reports two existing warnings (discarded timeout cancellation in example/autobahn/autobahn.go:58 and reflect.SliceHeader in wsutil/writer_test.go:135); both reproduce unchanged on the original PR head. Autobahn was not run locally.

CI update (October 25): the Go 1.27.3 jobs fail with wsflate: bad compressor: unexpected stream tail: 0xffff0300 vs 0x0000ffff. The same three compression tests fail on clean master fa8e8b8 and this head; the affected compression code is unchanged. This is tracked in #221 and the separate #227. The oldstable CI jobs on Ubuntu and macOS pass. The Autobahn workflow stops in its Go tests before running the Autobahn suite.

Comment thread write_test.go Outdated
"testing"
)

type writeFunc func([]byte) (int, error)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type writeFunc func([]byte) (int, error)
type writerFunc func([]byte) (int, error)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed the helper and its receiver to writerFunc in c4c3f25. The focused short-write tests and full race/coverage suite pass.

Comment thread write_test.go Outdated
}
return len(p), nil
})
if err := WriteFrame(w, NewTextFrame([]byte("hello"))); err != test.want {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here (and in the other test) we should do errors.Is() check to be forward compatible with WriteFrame() returning wrapped errors.

Same applies to the other test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both tests now use errors.Is in c4c3f25. The focused regressions and go test -race -shuffle=on -cover ./... pass on Go 1.26.10.

Comment thread write_test.go Outdated
{"header_short", 1, nil, io.ErrShortWrite},
{"header_error", 1, failure, failure},
{"payload_short", 2, nil, io.ErrShortWrite},
{"payload_error", 2, failure, failure},

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have all structs being initialised like:

{
  name: "...",
  call: 1,
  err: ...,
  ...
},
{
  name: "...",
  ...
}

This makes tests more readable and consistent with the current repo. Also, maybe call the call field something more precise, like "retErrAt" or "failAfter", so err would become failWith or failErr, while want can stay or be exp?

Same applies to the other test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded both tables into keyed multiline entries and renamed call/err to retErrAt/failWith in c4c3f25. The full race/coverage suite, build and go vet . pass. go vet ./... still reports two unrelated warnings, both reproduced on the original PR head; the description now distinguishes those results. Autobahn was not run locally.

Comment thread write_test.go Outdated
WriteHeader,
func(w io.Writer, h Header) error { return WriteHeaderBuffer(w, h, make([]byte, MaxHeaderSize)) },
} {
if err := write(w, Header{Fin: true, OpCode: OpText, Length: 5}); err != test.want {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if err := write(w, Header{Fin: true, OpCode: OpText, Length: 5}); err != test.want {
h := Header{
Fin: true,
OpCode: OpText,
Length: 5,
}
if err := write(w, h); err != test.want {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted the multiline Header literal as suggested in c4c3f25. The focused header/frame regressions and full race/coverage suite pass.

@gobwas

gobwas commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Hey thanks for this work! Just dropped a few comments.

@omrzk

omrzk commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

The Go 1.27.3 CI failures reproduce on clean master fa8e8b8: TestHelperWriteAndRead, TestWriter and TestFlateClientServer report the same unexpected stream tail: 0xffff0300 vs 0x0000ffff. This is tracked in #221 and separate PR #227. On this head, go test -race -shuffle=on -cover ./... passes the root package, including the short-write regressions, and wsutil; only wsflate and tests fail. The focused race run of those three tests also fails on clean master. Ubuntu/macOS oldstable CI jobs pass; Autobahn stops in make test before the suite starts. Updated the description with these results.

@gobwas
gobwas merged commit 3efce99 into gobwas:master Oct 26, 2026
2 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

why does conn.Write ignoring the number bytes written?

2 participants