Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5f42e6c

Browse files
author
Alex Dyukov
committed
Add base implementation
1 parent ae0e87f commit 5f42e6c

File tree

11 files changed

+834
-2
lines changed

11 files changed

+834
-2
lines changed

‎.github/tag_release.sh‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Its looks like semver, but its not, because we cannot release patch on prev major.minor release
2+
MAJOR_VERSION=0
3+
MAJOR_LAST_COMMIT_HASH="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
4+
5+
MINOR_LAST_COMMIT_HASH=$(git rev-list --invert-grep -i --grep='fix' ${MAJOR_LAST_COMMIT_HASH}..HEAD --no-merges -n 1)
6+
MINOR_VERSION=$(git rev-list --invert-grep -i --grep='fix' ${MAJOR_LAST_COMMIT_HASH}..HEAD --no-merges --count)
7+
8+
[ "${MINOR_LAST_COMMIT_HASH}" = "" ] && MINOR_VERSION="0"
9+
[ "${MINOR_LAST_COMMIT_HASH}" = "" ] && MINOR_LAST_COMMIT_HASH=${MAJOR_LAST_COMMIT_HASH}
10+
11+
PATCH_VERSION=$(git rev-list ${MINOR_LAST_COMMIT_HASH}..HEAD --no-merges --count)
12+
13+
APP_VERSION=v${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}
14+
15+
# get current commit hash for tag
16+
COMMIT_HASH=$(git rev-parse HEAD)
17+
18+
# POST a new ref to repo via Github API
19+
curl -s -X POST https://api.github.com/repos/${GITHUB_REPOSITORY}/git/refs \
20+
-H "Authorization: token ${GITHUB_TOKEN}" \
21+
-d @- << EOF
22+
{
23+
"ref": "refs/tags/${APP_VERSION}",
24+
"sha": "${COMMIT_HASH}"
25+
}
26+
EOF

‎.github/workflows/lint.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: lint
2+
on:
3+
push:
4+
branches-ignore:
5+
- master
6+
jobs:
7+
lint:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Install golang
11+
uses: actions/setup-go@v5
12+
with:
13+
go-version: 'oldstable'
14+
check-latest: true
15+
- name: Checkout git repository
16+
uses: actions/checkout@v4
17+
- name: Run linters
18+
uses: golangci/golangci-lint-action@v6

‎.github/workflows/tag.yml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: tag
2+
on:
3+
push:
4+
branches:
5+
- master
6+
jobs:
7+
tag:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Checkout git repository
11+
uses: actions/checkout@v4
12+
with:
13+
fetch-depth: 0
14+
- name: Tag latest release on github
15+
run: bash .github/tag_release.sh
16+
env:
17+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

‎.github/workflows/tests.yml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: tests
2+
on: push
3+
jobs:
4+
tests:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- name: Install golang
8+
uses: actions/setup-go@v5
9+
with:
10+
go-version: 'oldstable'
11+
check-latest: true
12+
- name: Checkout git repository
13+
uses: actions/checkout@v4
14+
- name: Run tests
15+
run: go test ./... -race -parallel 2 -shuffle on -v
16+
env:
17+
CGO_ENABLED: 1

‎README.md‎

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,82 @@
1-
# httpencoder
2-
Go net/http middleware for decode http.Request.Body and encode http.ResponseWriter based on Accept-Encoding and Content-Encoding headers
1+
# httpencoder - golang net/http middleware for decode requests and encode responses based on Accept-Encoding and Content-Encoding headers
2+
[![GoDoc](https://godoc.org/github.com/alexdyukov/httpencoder?status.svg)](https://godoc.org/github.com/alexdyukov/httpencoder)
3+
[![Tests](https://github.com/alexdyukov/httpencoder/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/alexdyukov/httpencoder/actions/workflows/tests.yml?query=branch%3Amaster)
4+
5+
## Decoding client body
6+
7+
According to RFCs there is no 'Accept-Encoding' header at server side response. It means you cannot tell clients (browsers, include headless browsers like curl/python's request) that your server accept any encodings. But some of the backends (for example [apache's mod_deflate](https://httpd.apache.org/docs/2.2/mod/mod_deflate.html#input)) support decoding request body, thats why the same feature exists in this package.
8+
9+
## Benchmarks
10+
11+
There is a little overhead to compare to `if strings.Contains(request.Header.Get("Accept-Encoding"), "myencoding")`
12+
```
13+
$ go test -bench=. -benchmem -benchtime=10000000x
14+
warning: GOPATH set to GOROOT (/home/user/go) has no effect
15+
goos: linux
16+
goarch: amd64
17+
pkg: github.com/alexdyukov/httpencoder
18+
cpu: AMD Ryzen 7 8845H w/ Radeon 780M Graphics
19+
BenchmarkRaw-16 10000000 226.3 ns/op 720 B/op 5 allocs/op
20+
BenchmarkRawEncode-16 10000000 251.3 ns/op 720 B/op 5 allocs/op
21+
BenchmarkWrappedEncodeDecode-16 10000000 905.7 ns/op 1537 B/op 13 allocs/op
22+
BenchmarkWrappedDecode-16 10000000 252.2 ns/op 720 B/op 5 allocs/op
23+
BenchmarkWrappedEncode-16 10000000 876.4 ns/op 1537 B/op 13 allocs/op
24+
PASS
25+
ok github.com/alexdyukov/httpencoder 25.135s
26+
```
27+
28+
## Examples
29+
30+
Gzip encoder/decoder:
31+
```
32+
33+
type gzipper struct{}
34+
35+
func (gzipper) Encode(ctx context.Context, to io.Writer, from []byte) (err error) {
36+
gzipWriter := gzip.NewWriter(to)
37+
38+
if _, err := gzipWriter.Write(from); err != nil {
39+
reqID := ctx.Value(contextValueKey)
40+
41+
slog.Info("failed to gzip response", "request_id", reqID, "error", err.Error())
42+
43+
return fmt.Errorf("Internal server error occur. Your request id %v. Try again later or feel free to contact us to get detailed info", reqID)
44+
}
45+
46+
if err := gzipWriter.Flush(); err != nil {
47+
reqID := ctx.Value(contextValueKey)
48+
49+
slog.Info("failed to flush gzipped response", "request_id", reqID, "error", err.Error())
50+
51+
return fmt.Errorf("Internal server error occur. Your request id %v. Try again later or feel free to contact us to get detailed info", reqID)
52+
}
53+
54+
return nil
55+
}
56+
57+
func (gzipper) Decode(ctx context.Context, to io.Writer, from []byte) (err error) {
58+
gzipReader, err := gzip.NewReader(bytes.NewReader(from))
59+
if err != nil {
60+
reqID := ctx.Value(contextValueKey)
61+
62+
slog.Info("failed to initialize gzip reader", "request_id", reqID, "error", err.Error())
63+
64+
return fmt.Errorf("Internal server error occur. Your request id %v. Try again later or feel free to contact us to get detailed info", reqID)
65+
}
66+
67+
_, err = io.Copy(to, gzipReader)
68+
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
69+
reqID := ctx.Value(contextValueKey)
70+
71+
slog.Info("failed to read from gzip reader", "request_id", reqID, "error", err.Error())
72+
73+
return fmt.Errorf("Internal server error occur. Your request id %v. Try again later or feel free to contact us to get detailed info", reqID)
74+
}
75+
76+
return nil
77+
}
78+
```
79+
80+
## License
81+
82+
MIT licensed. See the included LICENSE file for details.

‎benchmark_test.go‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package httpencoder_test
2+
3+
import (
4+
"bytes"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/alexdyukov/httpencoder"
10+
)
11+
12+
func BenchmarkRaw(b *testing.B) {
13+
b.StopTimer()
14+
15+
body := bytes.NewBufferString("abcd")
16+
request := httptest.NewRequest(http.MethodPost, "/", body)
17+
18+
handler := handlerWithoutEncoding
19+
20+
b.StartTimer()
21+
22+
for i := 0; i < b.N; i++ {
23+
handler.ServeHTTP(httptest.NewRecorder(), request)
24+
}
25+
}
26+
27+
func BenchmarkRawEncode(b *testing.B) {
28+
b.StopTimer()
29+
30+
body := bytes.NewBufferString("abcd")
31+
request := httptest.NewRequest(http.MethodPost, "/", body)
32+
request.Header.Set("Accept-Encoding", "repeate")
33+
34+
handler := handlerWithIfedEncoding
35+
36+
b.StartTimer()
37+
38+
for i := 0; i < b.N; i++ {
39+
handler.ServeHTTP(httptest.NewRecorder(), request)
40+
}
41+
}
42+
43+
func BenchmarkWrappedEncodeDecode(b *testing.B) {
44+
b.StopTimer()
45+
46+
body := bytes.NewBufferString("aabbccdd")
47+
request := httptest.NewRequest(http.MethodPost, "/", body)
48+
request.Header.Set("Accept-Encoding", "repeate")
49+
request.Header.Set("Content-Encoding", "repeate")
50+
51+
encoders := map[string]httpencoder.Encoder{"repeate": repeaterEntity}
52+
decoders := map[string]httpencoder.Decoder{"repeate": repeaterEntity}
53+
compress := httpencoder.New(encoders, decoders)
54+
handler := compress(handlerWithoutEncoding)
55+
56+
b.StartTimer()
57+
58+
for i := 0; i < b.N; i++ {
59+
handler.ServeHTTP(httptest.NewRecorder(), request)
60+
}
61+
}
62+
63+
func BenchmarkWrappedDecode(b *testing.B) {
64+
b.StopTimer()
65+
66+
body := bytes.NewBufferString("aabbccdd")
67+
request := httptest.NewRequest(http.MethodPost, "/", body)
68+
request.Header.Set("Content-Encoding", "repeate")
69+
70+
encoders := map[string]httpencoder.Encoder{}
71+
decoders := map[string]httpencoder.Decoder{"repeate": repeaterEntity}
72+
compress := httpencoder.New(encoders, decoders)
73+
handler := compress(handlerWithoutEncoding)
74+
75+
b.StartTimer()
76+
77+
for i := 0; i < b.N; i++ {
78+
handler.ServeHTTP(httptest.NewRecorder(), request)
79+
}
80+
}
81+
82+
func BenchmarkWrappedEncode(b *testing.B) {
83+
b.StopTimer()
84+
85+
body := bytes.NewBufferString("abcd")
86+
request := httptest.NewRequest(http.MethodPost, "/", body)
87+
request.Header.Set("Accept-Encoding", "repeate")
88+
89+
encoders := map[string]httpencoder.Encoder{"repeate": repeaterEntity}
90+
decoders := map[string]httpencoder.Decoder{}
91+
compress := httpencoder.New(encoders, decoders)
92+
handler := compress(handlerWithoutEncoding)
93+
94+
b.StartTimer()
95+
96+
for i := 0; i < b.N; i++ {
97+
handler.ServeHTTP(httptest.NewRecorder(), request)
98+
}
99+
}

‎decode.go‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package httpencoder
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"sync"
7+
)
8+
9+
func decode(bufferPool *sync.Pool, decoders map[string]Decoder, next http.Handler) http.Handler {
10+
if len(decoders) == 0 {
11+
return next
12+
}
13+
14+
return http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
15+
header := compactAndLow([]byte(request.Header.Get("Content-Encoding")))
16+
if len(header) == 0 {
17+
next.ServeHTTP(responseWriter, request)
18+
19+
return
20+
}
21+
22+
bodyBuffer := bufferGet(bufferPool)
23+
defer bufferPut(bufferPool, bodyBuffer)
24+
25+
if _, err := bodyBuffer.ReadFrom(request.Body); err != nil {
26+
http.Error(responseWriter, "failed to read http request body", http.StatusBadRequest)
27+
28+
return
29+
}
30+
31+
for iter := 0; iter < len(header); iter++ {
32+
start := iter
33+
34+
for iter < len(header) && isAlpha(header[iter]) {
35+
iter++
36+
}
37+
38+
decoder, exist := decoders[string(header[start:iter])]
39+
if !exist {
40+
http.Error(responseWriter, "unsupported Content-Encoding", http.StatusUnsupportedMediaType)
41+
42+
return
43+
}
44+
45+
content := bodyBuffer.Bytes()
46+
bodyBuffer.Reset()
47+
48+
if err := decoder.Decode(request.Context(), bodyBuffer, content); err != nil {
49+
http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
50+
51+
return
52+
}
53+
}
54+
55+
request.Body = io.NopCloser(bodyBuffer)
56+
request.Header.Del("Content-Encoding")
57+
58+
next.ServeHTTP(responseWriter, request)
59+
})
60+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /