Skip to content

Commit c85ebbe

Browse files
ArthurSensskandragoncodebotenmx-psiperebaj
authored
Fix snappy/framed-snappy encoding/decoding (#12911)
#### Description This is an alternative PR to #12825. I'm taking all the commits from that PR and adding the feature gate on the client side, as requested by reviews. The server behavior of peeking into the initial bytes to identify the encoding is kept :) If you've reviewed the previous PR, you can just review the latest commit! <!-- Issue number if applicable --> #### Link to tracking issue Fixes #10584 --------- Signed-off-by: Arthur Silva Sens <[email protected]> Co-authored-by: Michael Graff <[email protected]> Co-authored-by: Alex Boten <[email protected]> Co-authored-by: Pablo Baeyens <[email protected]> Co-authored-by: Jonathan <[email protected]>
1 parent 829157c commit c85ebbe

File tree

9 files changed

+265
-33
lines changed

9 files changed

+265
-33
lines changed

.chloggen/snappy-done-right.yaml

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: confighttp and configcompression
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Fix handling of `snappy` content-encoding in a backwards-compatible way"
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [10584, 12825]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: |
19+
The collector used the Snappy compression type of "framed" to handle the HTTP
20+
content-encoding "snappy". However, this encoding is typically used to indicate
21+
the "block" compression variant of "snappy". This change allows the collector to:
22+
- When receiving a request with encoding 'snappy', the server endpoints will peek
23+
at the first bytes of the payload to determine if it is "framed" or "block" snappy,
24+
and will decompress accordingly. This is a backwards-compatible change.
25+
26+
If the feature-gate "confighttp.framedSnappy" is enabled, you'll see new behavior for both client and server:
27+
- Client compression type "snappy" will now compress to the "block" variant of snappy
28+
instead of "framed". Client compression type "x-snappy-framed" will now compress to the "framed" variant of snappy.
29+
- Servers will accept both "snappy" and "x-snappy-framed" as valid content-encodings.
30+
31+
# Optional: The change log or logs in which this entry should be included.
32+
# e.g. '[user]' or '[user, api]'
33+
# Include 'user' if the change is relevant to end users.
34+
# Include 'api' if there is a change to a library API.
35+
# Default: '[user]'
36+
change_logs: [user]

config/configcompression/compressiontype.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const (
2424
TypeZlib Type = "zlib"
2525
TypeDeflate Type = "deflate"
2626
TypeSnappy Type = "snappy"
27+
TypeSnappyFramed Type = "x-snappy-framed"
2728
TypeZstd Type = "zstd"
2829
TypeLz4 Type = "lz4"
2930
typeNone Type = "none"
@@ -43,6 +44,7 @@ func (ct *Type) UnmarshalText(in []byte) error {
4344
typ == TypeZlib ||
4445
typ == TypeDeflate ||
4546
typ == TypeSnappy ||
47+
typ == TypeSnappyFramed ||
4648
typ == TypeZstd ||
4749
typ == TypeLz4 ||
4850
typ == typeNone ||

config/configcompression/compressiontype_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ func TestUnmarshalText(t *testing.T) {
4242
shouldError: false,
4343
isCompressed: true,
4444
},
45+
{
46+
name: "ValidSnappyFramed",
47+
compressionName: []byte("x-snappy-framed"),
48+
shouldError: false,
49+
isCompressed: true,
50+
},
4551
{
4652
name: "ValidZstd",
4753
compressionName: []byte("zstd"),
@@ -128,6 +134,17 @@ func TestValidateParams(t *testing.T) {
128134
compressionLevel: 1,
129135
shouldError: true,
130136
},
137+
{
138+
name: "ValidSnappyFramed",
139+
compressionName: []byte("x-snappy-framed"),
140+
shouldError: false,
141+
},
142+
{
143+
name: "InvalidSnappyFramed",
144+
compressionName: []byte("x-snappy-framed"),
145+
compressionLevel: 1,
146+
shouldError: true,
147+
},
131148
{
132149
name: "ValidZstd",
133150
compressionName: []byte("zstd"),

config/confighttp/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ README](../configtls/README.md).
4848
- SpeedBestCompression: `11`
4949
- `snappy`
5050
No compression levels supported yet
51+
- `x-snappy-framed` (When feature gate `confighttp.framedSnappy` is enabled)
52+
No compression levels supported yet
5153
- [`max_idle_conns`](https://golang.org/pkg/net/http/#Transport)
5254
- [`max_idle_conns_per_host`](https://golang.org/pkg/net/http/#Transport)
5355
- [`max_conns_per_host`](https://golang.org/pkg/net/http/#Transport)
@@ -105,6 +107,7 @@ will not be enabled.
105107
- `endpoint`: Valid value syntax available [here](https://github.com/grpc/grpc/blob/master/doc/naming.md)
106108
- `max_request_body_size`: configures the maximum allowed body size in bytes for a single request. Default: `20971520` (20MiB)
107109
- `compression_algorithms`: configures the list of compression algorithms the server can accept. Default: ["", "gzip", "zstd", "zlib", "snappy", "deflate", "lz4"]
110+
- `x-snappy-framed` can be used if feature gate `confighttp.snappyFramed` is enabled.
108111
- [`tls`](../configtls/README.md)
109112
- [`auth`](../configauth/README.md)
110113
- `request_params`: a list of query parameter names to add to the auth context, along with the HTTP headers

config/confighttp/compression.go

+61-5
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
package confighttp // import "go.opentelemetry.io/collector/config/confighttp"
77

88
import (
9+
"bufio"
910
"bytes"
1011
"compress/gzip"
1112
"compress/zlib"
13+
"errors"
1214
"fmt"
1315
"io"
1416
"net/http"
@@ -18,6 +20,15 @@ import (
1820
"github.com/pierrec/lz4/v4"
1921

2022
"go.opentelemetry.io/collector/config/configcompression"
23+
"go.opentelemetry.io/collector/featuregate"
24+
)
25+
26+
var enableFramedSnappy = featuregate.GlobalRegistry().MustRegister(
27+
"confighttp.framedSnappy",
28+
featuregate.StageAlpha,
29+
featuregate.WithRegisterFromVersion("v0.125.0"),
30+
featuregate.WithRegisterDescription("Content encoding 'snappy' will compress/decompress block snappy format while 'x-snappy-framed' will compress/decompress framed snappy format."),
31+
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector/issues/10584"),
2132
)
2233

2334
type compressRoundTripper struct {
@@ -60,23 +71,65 @@ var availableDecoders = map[string]func(body io.ReadCloser) (io.ReadCloser, erro
6071
}
6172
return zr, nil
6273
},
74+
"snappy": snappyHandler,
6375
//nolint:unparam // Ignoring the linter request to remove error return since it needs to match the method signature
64-
"snappy": func(body io.ReadCloser) (io.ReadCloser, error) {
65-
// Lazy Reading content to improve memory efficiency
76+
"lz4": func(body io.ReadCloser) (io.ReadCloser, error) {
6677
return &compressReadCloser{
67-
Reader: snappy.NewReader(body),
78+
Reader: lz4.NewReader(body),
6879
orig: body,
6980
}, nil
7081
},
7182
//nolint:unparam // Ignoring the linter request to remove error return since it needs to match the method signature
72-
"lz4": func(body io.ReadCloser) (io.ReadCloser, error) {
83+
"x-snappy-framed": func(body io.ReadCloser) (io.ReadCloser, error) {
7384
return &compressReadCloser{
74-
Reader: lz4.NewReader(body),
85+
Reader: snappy.NewReader(body),
7586
orig: body,
7687
}, nil
7788
},
7889
}
7990

91+
// snappyFramingHeader is always the first 10 bytes of a snappy framed stream.
92+
var snappyFramingHeader = []byte{
93+
0xff, 0x06, 0x00, 0x00,
94+
0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, // "sNaPpY"
95+
}
96+
97+
// snappyHandler returns an io.ReadCloser that auto-detects the snappy format.
98+
// This is necessary because the collector previously used "content-encoding: snappy"
99+
// but decompressed and compressed the payloads using the snappy framing format.
100+
// However, "content-encoding: snappy" is uses the block format, and "x-snappy-framed"
101+
// is the framing format. This handler is a (hopefully temporary) hack to
102+
// make this work in a backwards-compatible way.
103+
//
104+
// See https://github.com/google/snappy/blob/6af9287fbdb913f0794d0148c6aa43b58e63c8e3/framing_format.txt#L27-L36
105+
// for more details on the framing format.
106+
func snappyHandler(body io.ReadCloser) (io.ReadCloser, error) {
107+
br := bufio.NewReader(body)
108+
109+
peekBytes, err := br.Peek(len(snappyFramingHeader))
110+
if err != nil && !errors.Is(err, io.EOF) {
111+
return nil, err
112+
}
113+
114+
isFramed := len(peekBytes) >= len(snappyFramingHeader) && bytes.Equal(peekBytes[:len(snappyFramingHeader)], snappyFramingHeader)
115+
116+
if isFramed {
117+
return &compressReadCloser{
118+
Reader: snappy.NewReader(br),
119+
orig: body,
120+
}, nil
121+
}
122+
compressed, err := io.ReadAll(br)
123+
if err != nil {
124+
return nil, err
125+
}
126+
decoded, err := snappy.Decode(nil, compressed)
127+
if err != nil {
128+
return nil, err
129+
}
130+
return io.NopCloser(bytes.NewReader(decoded)), nil
131+
}
132+
80133
func newCompressionParams(level configcompression.Level) configcompression.CompressionParams {
81134
return configcompression.CompressionParams{
82135
Level: level,
@@ -143,6 +196,9 @@ func httpContentDecompressor(h http.Handler, maxRequestBodySize int64, eh func(w
143196

144197
enabled := map[string]func(body io.ReadCloser) (io.ReadCloser, error){}
145198
for _, dec := range enableDecoders {
199+
if dec == "x-frame-snappy" && !enableFramedSnappy.IsEnabled() {
200+
continue
201+
}
146202
enabled[dec] = availableDecoders[dec]
147203

148204
if dec == "deflate" {

0 commit comments

Comments
 (0)