Skip to content

Commit 890943d

Browse files
authored
feat(range): mvp support (#653)
* feat(range): mvp support * feat(cache-tests): mostly pass combining partial content
1 parent b2a08bf commit 890943d

File tree

4 files changed

+196
-6
lines changed

4 files changed

+196
-6
lines changed

.github/workflows/non-regression.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ jobs:
5050
name: Build the stack
5151
run: docker compose -f docker-compose.yml.prod up -d --build --force-recreate --remove-orphans
5252
-
53-
name: Souin container healthceck
53+
name: Souin container healthcheck
5454
run: docker compose -f docker-compose.yml.prod exec -T souin ls

pkg/middleware/middleware.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"sync"
1515
"time"
1616

17-
xxhash "github.com/cespare/xxhash/v2"
17+
"github.com/cespare/xxhash/v2"
1818
"github.com/darkweak/souin/configurationtypes"
1919
"github.com/darkweak/souin/context"
2020
"github.com/darkweak/souin/helpers"
@@ -730,7 +730,11 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n
730730
bufPool := s.bufPool.Get().(*bytes.Buffer)
731731
bufPool.Reset()
732732
defer s.bufPool.Put(bufPool)
733+
733734
customWriter := NewCustomWriter(req, rw, bufPool)
735+
customWriter.Headers.Add("Range", req.Header.Get("Range"))
736+
// req.Header.Del("Range")
737+
734738
go func(req *http.Request, crw *CustomWriter) {
735739
<-req.Context().Done()
736740
crw.mutex.Lock()

pkg/middleware/writer.go

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package middleware
22

33
import (
44
"bytes"
5+
"fmt"
56
"net/http"
67
"strconv"
8+
"strings"
79
"sync"
810

911
"github.com/darkweak/go-esi/esi"
@@ -85,6 +87,59 @@ func (r *CustomWriter) Write(b []byte) (int, error) {
8587
return len(b), nil
8688
}
8789

90+
type rangeValue struct {
91+
from, to int64
92+
}
93+
94+
const separator = "--SOUIN-HTTP-CACHE-SEPARATOR"
95+
96+
func parseRange(rangeHeaders []string, contentRange string) ([]rangeValue, rangeValue, int64) {
97+
if len(rangeHeaders) == 0 {
98+
return nil, rangeValue{}, -1
99+
}
100+
101+
crv := rangeValue{from: 0, to: 0}
102+
var total int64 = -1
103+
if contentRange != "" {
104+
crVal := strings.Split(strings.TrimPrefix(contentRange, "bytes "), "/")
105+
total, _ = strconv.ParseInt(crVal[1], 10, 64)
106+
total--
107+
108+
crSplit := strings.Split(crVal[0], "-")
109+
crv.from, _ = strconv.ParseInt(crSplit[0], 10, 64)
110+
crv.to, _ = strconv.ParseInt(crSplit[1], 10, 64)
111+
}
112+
113+
values := make([]rangeValue, len(rangeHeaders))
114+
115+
for idx, header := range rangeHeaders {
116+
ranges := strings.Split(header, "-")
117+
rv := rangeValue{from: -1, to: total}
118+
119+
// e.g. Range: -5
120+
if len(ranges) == 2 && ranges[0] == "" {
121+
ranges[0] = "-" + ranges[1]
122+
from, _ := strconv.ParseInt(ranges[0], 10, 64)
123+
rv.from = total + from
124+
125+
values[idx] = rv
126+
127+
continue
128+
}
129+
130+
rv.from, _ = strconv.ParseInt(ranges[0], 10, 64)
131+
132+
if ranges[1] != "" {
133+
rv.to, _ = strconv.ParseInt(ranges[1], 10, 64)
134+
rv.to++
135+
}
136+
137+
values[idx] = rv
138+
}
139+
140+
return values, crv, total + 1
141+
}
142+
88143
// Send delays the response to handle Cache-Status
89144
func (r *CustomWriter) Send() (int, error) {
90145
defer r.handleBuffer(func(b *bytes.Buffer) {
@@ -94,10 +149,72 @@ func (r *CustomWriter) Send() (int, error) {
94149
if storedLength != "" {
95150
r.Header().Set("Content-Length", storedLength)
96151
}
97-
b := esi.Parse(r.Buf.Bytes(), r.Req)
98-
if len(b) != 0 {
99-
r.Header().Set("Content-Length", strconv.Itoa(len(b)))
152+
153+
result := r.Buf.Bytes()
154+
155+
result = esi.Parse(result, r.Req)
156+
157+
if r.Headers.Get("Range") != "" {
158+
159+
var bufStr string
160+
mimeType := r.Header().Get("Content-Type")
161+
162+
r.WriteHeader(http.StatusPartialContent)
163+
164+
rangeHeader, contentRangeValue, total := parseRange(
165+
strings.Split(strings.TrimPrefix(r.Headers.Get("Range"), "bytes="), ", "),
166+
r.Header().Get("Content-Range"),
167+
)
168+
bodyBytes := r.Buf.Bytes()
169+
bufLen := int64(r.Buf.Len())
170+
if total > 0 {
171+
bufLen = total
172+
}
173+
174+
if len(rangeHeader) == 1 {
175+
header := rangeHeader[0]
176+
internalFrom := (header.from - contentRangeValue.from) % bufLen
177+
internalTo := (header.to - contentRangeValue.from) % bufLen
178+
179+
content := bodyBytes[internalFrom:]
180+
181+
r.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", contentRangeValue.from, contentRangeValue.to, bufLen))
182+
183+
if internalTo >= 0 {
184+
content = content[:internalTo-internalFrom]
185+
r.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", header.from, header.to, bufLen))
186+
}
187+
188+
result = content
189+
}
190+
191+
if len(rangeHeader) > 1 {
192+
r.Header().Set("Content-Type", "multipart/byteranges; boundary="+separator)
193+
194+
for _, header := range rangeHeader {
195+
196+
content := bodyBytes[header.from:]
197+
if header.to >= 0 {
198+
content = content[:header.to-header.from]
199+
}
200+
201+
bufStr += fmt.Sprintf(`
202+
%s
203+
Content-Type: %s
204+
Content-Range: bytes %d-%d/%d
205+
206+
%s
207+
`, separator, mimeType, header.from, header.to, r.Buf.Len(), content)
208+
}
209+
210+
result = []byte(bufStr + separator + "--")
211+
}
212+
}
213+
214+
if len(result) != 0 {
215+
r.Header().Set("Content-Length", strconv.Itoa(len(result)))
100216
}
217+
101218
r.Header().Del(rfc.StoredLengthHeader)
102219
r.Header().Del(rfc.StoredTTLHeader)
103220

@@ -106,5 +223,5 @@ func (r *CustomWriter) Send() (int, error) {
106223
r.headersSent = true
107224
}
108225

109-
return r.Rw.Write(b)
226+
return r.Rw.Write(result)
110227
}

plugins/caddy/httpcache_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,3 +1504,72 @@ func TestAPIPlatformInvalidation(t *testing.T) {
15041504
t.Errorf("unexpected list %#v", items)
15051505
}
15061506
}
1507+
1508+
func TestRange(t *testing.T) {
1509+
tester := caddytest.NewTester(t)
1510+
tester.InitServer(`
1511+
{
1512+
debug
1513+
admin localhost:2999
1514+
http_port 9080
1515+
cache {
1516+
api {
1517+
souin
1518+
}
1519+
}
1520+
}
1521+
localhost:9080 {
1522+
route /range-request {
1523+
cache
1524+
1525+
respond "Hello range-request!"
1526+
}
1527+
}`, "caddyfile")
1528+
1529+
reqRange, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/range-request", nil)
1530+
reqRange.Header.Set("Range", "bytes=0-4, 6-10")
1531+
1532+
resp1, _ := tester.AssertResponse(reqRange, http.StatusPartialContent, `
1533+
--SOUIN-HTTP-CACHE-SEPARATOR
1534+
Content-Type: text/plain; charset=utf-8
1535+
Content-Range: bytes 0-5/20
1536+
1537+
Hello
1538+
1539+
--SOUIN-HTTP-CACHE-SEPARATOR
1540+
Content-Type: text/plain; charset=utf-8
1541+
Content-Range: bytes 6-11/20
1542+
1543+
range
1544+
--SOUIN-HTTP-CACHE-SEPARATOR--`)
1545+
1546+
if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/range-request" {
1547+
t.Errorf("unexpected resp1 Cache-Status header %v", resp1.Header.Get("Cache-Status"))
1548+
}
1549+
1550+
if resp1.Header.Get("Age") != "" {
1551+
t.Errorf("unexpected resp1 Age header %v", resp1.Header.Get("Age"))
1552+
}
1553+
1554+
resp2, _ := tester.AssertResponse(reqRange, http.StatusPartialContent, `
1555+
--SOUIN-HTTP-CACHE-SEPARATOR
1556+
Content-Type: text/plain; charset=utf-8
1557+
Content-Range: bytes 0-5/20
1558+
1559+
Hello
1560+
1561+
--SOUIN-HTTP-CACHE-SEPARATOR
1562+
Content-Type: text/plain; charset=utf-8
1563+
Content-Range: bytes 6-11/20
1564+
1565+
range
1566+
--SOUIN-HTTP-CACHE-SEPARATOR--`)
1567+
1568+
if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=119; key=GET-http-localhost:9080-/range-request; detail=DEFAULT" {
1569+
t.Errorf("unexpected resp2 Cache-Status header %v", resp2.Header.Get("Cache-Status"))
1570+
}
1571+
1572+
if resp2.Header.Get("Age") != "1" {
1573+
t.Errorf("unexpected resp2 Age header %v", resp2.Header.Get("Age"))
1574+
}
1575+
}

0 commit comments

Comments
 (0)