Skip to content

Commit 0c3be52

Browse files
authored
feat(store/fscache): add optional mtime update on cache hits (#20)
2 parents 1cc8851 + 0cb2fde commit 0c3be52

16 files changed

Lines changed: 227 additions & 82 deletions

.github/workflows/default.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: Test
22
on:
33
push:
4+
branches: [master]
45
pull_request:
56
workflow_dispatch:
67

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
## Features
1313

1414
- **Plug-and-Play**: Just swap in as your HTTP client's transport; no extra configuration needed. [^1]
15-
- **RFC 9111 Compliance**: Handles validation, expiration, and revalidation ([see details](#rfc-9111-compliance-matrix)).
16-
- **Cache Control**: Supports all required HTTP cache control directives, as well as extensions like `stale-while-revalidate`, `stale-if-error`, and `immutable` ([view details](#field-definitions-details)).
15+
- **RFC 9111 Compliance**: Handles validation, expiration, and revalidation (see [details](#rfc-9111-compliance-matrix)).
16+
- **Cache Control**: Supports all required HTTP cache control directives, as well as extensions like `stale-while-revalidate`, `stale-if-error`, and `immutable` (see [details](#field-definitions-details)).
1717
- **Cache Backends**: Built-in support for file system and memory caches, with the ability to implement custom backends (see [Cache Backends](#cache-backends)).
1818
- **Cache Maintenance API**: Optional REST endpoints for listing, retrieving, and deleting cache entries (see [Cache Maintenance API](#cache-maintenance-api-debug-only)).
1919
- **Extensible**: Options for logging, transport and timeouts (see [Options](#options)).
@@ -264,7 +264,7 @@ Content-Type: application/json
264264
| 5.2.1.3. | `min-fresh` | Optional | ✔️ | |
265265
| 5.2.1.4. | `no-cache` | Optional | ✔️ | |
266266
| 5.2.1.5. | `no-store` | Optional | ✔️ | |
267-
| 5.2.1.6. | `no-transform` | Optional | ✔️ | Compliant by default - implementation never transforms content |
267+
| 5.2.1.6. | `no-transform` | Optional | ✔️ | See [Content Transformation Compliance](#content-transformation-compliance) |
268268
| 5.2.1.7. | `only-if-cached` | Optional | ✔️ | |
269269

270270
</details>
@@ -283,7 +283,7 @@ Content-Type: application/json
283283
| 5.2.2.3. | `must-understand` | Required | ✔️ | |
284284
| 5.2.2.4. | `no-cache` | Required | ✔️ | Both qualified and unqualified forms supported |
285285
| 5.2.2.5. | `no-store` | Required | ✔️ | |
286-
| 5.2.2.6. | `no-transform` | Required | ✔️ | Compliant by default - implementation never transforms content |
286+
| 5.2.2.6. | `no-transform` | Required | ✔️ | See [Content Transformation Compliance](#content-transformation-compliance) |
287287
| 5.2.2.7. | `private` | N/A | N/A | Intended for shared caches; not applicable to private caches |
288288
| 5.2.2.8. | `proxy-revalidate` | N/A | N/A | Intended for shared caches; not applicable to private caches |
289289
| 5.2.2.9. | `public` | Optional | ✔️ | |
@@ -310,6 +310,22 @@ The following additional cache control directives are supported, as defined in v
310310
</details>
311311
</details>
312312

313+
### Content Transformation Compliance
314+
315+
This implementation maintains RFC 9111 compliance regarding the `no-transform` directive by enforcing a **storage-only** architecture:
316+
317+
```go
318+
type Conn interface {
319+
Get(key string) ([]byte, error) // Returns byte-identical data
320+
Set(key string, data []byte) error // Stores byte-identical data
321+
Delete(key string) error // Simple deletion
322+
}
323+
```
324+
325+
**Storage optimizations** (compression, encryption, serialization) are allowed because they return **byte-identical** HTTP responses. This differs from transformations mentioned in [RFC 9110 §7.7](https://www.rfc-editor.org/rfc/rfc9110#section-7.7), like image format conversion which change actual response content.
326+
327+
**Custom backends:** Must return byte-identical responses. Use the [`store/acceptance`](https://pkg.go.dev/github.com/bartventer/httpcache/store/acceptance) test suite to verify compliance.
328+
313329
## License
314330

315331
This project is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). See the [LICENSE](LICENSE) file for details.

helpers.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ import (
2424

2525
func make504Response(req *http.Request) (*http.Response, error) {
2626
var buf bytes.Buffer
27-
buf.WriteString("HTTP/1.1 504 Gateway Timeout\r\n")
28-
buf.WriteString("Cache-Control: no-cache\r\n")
29-
buf.WriteString("Content-Length: 0\r\n")
30-
buf.WriteString(
27+
_, _ = buf.WriteString("HTTP/1.1 504 Gateway Timeout\r\n")
28+
_, _ = buf.WriteString("Cache-Control: no-cache\r\n")
29+
_, _ = buf.WriteString("Content-Length: 0\r\n")
30+
_, _ = buf.WriteString(
3131
internal.CacheStatusHeader + ": " + internal.CacheStatusBypass.Value + "\r\n",
3232
)
33-
buf.WriteString("Connection: close\r\n")
34-
buf.WriteString("\r\n")
33+
_, _ = buf.WriteString("Connection: close\r\n")
34+
_, _ = buf.WriteString("\r\n")
3535
return http.ReadResponse(bufio.NewReader(&buf), req)
3636
}
3737

internal/log.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -281,12 +281,12 @@ func (l *Logger) logCache(ctx context.Context, level slog.Level, msg string, lp
281281
attrs = append(attrs, slog.String("trace_id", tracedID))
282282
}
283283
attrs = append(attrs,
284-
groupAttrs("request",
284+
slog.GroupAttrs("request",
285285
slog.String("method", req.Method),
286286
slog.String("url", req.URL.String()),
287287
slog.String("host", req.Host),
288288
),
289-
groupAttrs("cache",
289+
slog.GroupAttrs("cache",
290290
slog.Any("status", event),
291291
slog.String("url_key", cl.URLKey),
292292
),
@@ -303,13 +303,3 @@ func (l *Logger) logCache(ctx context.Context, level slog.Level, msg string, lp
303303
r.AddAttrs(attrs...)
304304
_ = l.handler.Handle(ctx, r)
305305
}
306-
307-
/*
308-
+-------------------------------+
309-
| Helpers |
310-
+-------------------------------+
311-
*/
312-
313-
func groupAttrs(key string, args ...slog.Attr) slog.Attr {
314-
return slog.Attr{Key: key, Value: slog.GroupValue(args...)}
315-
}

internal/normalization.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,8 @@ func makeVaryHash(vary map[string]string) uint64 {
343343
keys = slices.AppendSeq(keys, maps.Keys(vary))
344344
slices.Sort(keys)
345345
for _, k := range keys {
346-
h.Write([]byte(k))
347-
h.Write([]byte(vary[k]))
346+
_, _ = h.Write([]byte(k))
347+
_, _ = h.Write([]byte(vary[k]))
348348
}
349349
return h.Sum64()
350350
}

internal/varymatcher.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,22 @@ func (vm *varyMatcher) VaryHeadersMatch(entries ResponseRefs, reqHdr http.Header
4141
bVary := strings.TrimSpace(b.Vary)
4242

4343
// Responses with Vary: "*" are least preferred
44-
aIsStar := aVary == "*"
45-
bIsStar := bVary == "*"
46-
if aIsStar && !bIsStar {
44+
switch aIsStar, bIsStar := aVary == "*", bVary == "*"; {
45+
case aIsStar && !bIsStar:
4746
return 1 // b preferred
48-
}
49-
if bIsStar && !aIsStar {
47+
case bIsStar && !aIsStar:
5048
return -1 // a preferred
5149
}
5250

5351
// Responses with Vary headers are preferred over those without
54-
aHasVary := aVary != ""
55-
bHasVary := bVary != ""
56-
if aHasVary && !bHasVary {
52+
switch aHasVary, bHasVary := aVary != "", bVary != ""; {
53+
case aHasVary && !bHasVary:
5754
return -1 // a preferred
58-
}
59-
if !aHasVary && bHasVary {
55+
case !aHasVary && bHasVary:
6056
return 1 // b preferred
6157
}
6258

63-
// If both have Vary headers, sort by Date or ResponseTime
59+
// If both have Vary headers, sort by ReceivedAt (earliest first)
6460
return a.ReceivedAt.Compare(b.ReceivedAt)
6561
})
6662

0 commit comments

Comments
 (0)