feat(gw): Cache-Control: only-if-cached
This implements the only-if-cached behavior documented in specs: https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#only-if-cached-head-behavior
Marcin Rataj committed
Jul 5, 2022 at 15:39 UTC
58aaee00f80cf913174cf47f8c40367b70b8a5df
3 files changed
+67
-7
core/corehttp/gateway.go
+5
-1
@@ -92,12 +92,16 @@ func GatewayOption(writable bool, paths ...string) ServeOption {
92
"X-Ipfs-Roots",
93
}, headers[ACEHeadersName]...))
94
95
- var gateway http.Handler = newGatewayHandler(GatewayConfig{
95
+ var gateway http.Handler
96
+ gateway, err = newGatewayHandler(GatewayConfig{
97
Headers: headers,
98
Writable: writable,
99
PathPrefixes: cfg.Gateway.PathPrefixes,
100
FastDirIndexThreshold: int(cfg.Gateway.FastDirIndexThreshold.WithDefault(100)),
101
}, api)
102
+ if err != nil {
103
+ return nil, err
104
+ }
105
106
gateway = otelhttp.NewHandler(gateway, "Gateway.Request")
107
core/corehttp/gateway_handler.go
+40
-6
@@ -24,6 +24,7 @@ import (
24
path "github.com/ipfs/go-path"
25
"github.com/ipfs/go-path/resolver"
26
coreiface "github.com/ipfs/interface-go-ipfs-core"
27
+ options "github.com/ipfs/interface-go-ipfs-core/options"
28
ipath "github.com/ipfs/interface-go-ipfs-core/path"
29
routing "github.com/libp2p/go-libp2p-core/routing"
30
prometheus "github.com/prometheus/client_golang/prometheus"
@@ -66,8 +67,9 @@ type redirectTemplateData struct {
67
// gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/<path>)
68
// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link)
69
type gatewayHandler struct {
69
- config GatewayConfig
70
- api coreiface.CoreAPI
70
+ config GatewayConfig
71
+ api coreiface.CoreAPI
72
+ offlineApi coreiface.CoreAPI
73
74
// generic metrics
75
firstContentBlockGetMetric *prometheus.HistogramVec
@@ -211,10 +213,15 @@ func newGatewayHistogramMetric(name string, help string) *prometheus.HistogramVe
213
return histogramMetric
214
}
215
214
-func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
216
+func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) (*gatewayHandler, error) {
217
+ offlineApi, err := api.WithOptions(options.Api.Offline(true))
218
+ if err != nil {
219
+ return nil, err
220
+ }
221
i := &gatewayHandler{
216
- config: c,
217
- api: api,
222
+ config: c,
223
+ api: api,
224
+ offlineApi: offlineApi,
225
// Improved Metrics
226
// ----------------------------
227
// Time till the first content block (bar in /ipfs/cid/foo/bar)
@@ -255,7 +262,7 @@ func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
262
"The time to receive the first UnixFS node on a GET from the gateway.",
263
),
264
}
258
- return i
265
+ return i, nil
266
}
267
268
func parseIpfsPath(p string) (cid.Cid, string, error) {
@@ -360,6 +367,11 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
367
}
368
369
contentPath := ipath.New(r.URL.Path)
370
+
371
+ if requestHandled := i.handleOnlyIfCached(w, r, contentPath, logger); requestHandled {
372
+ return
373
+ }
374
+
375
if requestHandled := handleSuperfluousNamespace(w, r, contentPath); requestHandled {
376
return
377
}
@@ -956,6 +968,28 @@ func debugStr(path string) string {
968
return q
969
}
970
971
+// Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore.
972
+// https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header
973
+func (i *gatewayHandler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) (requestHandled bool) {
974
+ if r.Header.Get("Cache-Control") == "only-if-cached" {
975
+ _, err := i.offlineApi.Block().Stat(r.Context(), contentPath)
976
+ if err != nil {
977
+ if r.Method == http.MethodHead {
978
+ w.WriteHeader(http.StatusPreconditionFailed)
979
+ return true
980
+ }
981
+ errMsg := fmt.Sprintf("%q not in local datastore", contentPath.String())
982
+ http.Error(w, errMsg, http.StatusPreconditionFailed)
983
+ return true
984
+ }
985
+ if r.Method == http.MethodHead {
986
+ w.WriteHeader(http.StatusOK)
987
+ return true
988
+ }
989
+ }
990
+ return false
991
+}
992
+
993
func handleUnsupportedHeaders(r *http.Request) (err *requestError) {
994
// X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/kubo/issues/7702)
995
// TODO: remove this after go-ipfs 0.13 ships
test/sharness/t0116-gateway-cache.sh
+22
@@ -67,6 +67,28 @@ test_expect_success "Prepare IPNS unixfs content path for testing" '
67
cat curl_ipns_file_output
68
'
69
70
+# Cache-Control: only-if-cached
71
+ test_expect_success "HEAD for /ipfs/ with only-if-cached succeeds when in local datastore" '
72
+ curl -sv -I -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/index.html" > curl_onlyifcached_postitive_head 2>&1 &&
73
+ cat curl_onlyifcached_postitive_head &&
74
+ grep "< HTTP/1.1 200 OK" curl_onlyifcached_postitive_head
75
+ '
76
+ test_expect_success "HEAD for /ipfs/ with only-if-cached fails when not in local datastore" '
77
+ curl -sv -I -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$(date | ipfs add --only-hash -Q)" > curl_onlyifcached_negative_head 2>&1 &&
78
+ cat curl_onlyifcached_negative_head &&
79
+ grep "< HTTP/1.1 412 Precondition Failed" curl_onlyifcached_negative_head
80
+ '
81
+ test_expect_success "GET for /ipfs/ with only-if-cached succeeds when in local datastore" '
82
+ curl -svX GET -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/index.html" >/dev/null 2>curl_onlyifcached_postitive_out &&
83
+ cat curl_onlyifcached_postitive_out &&
84
+ grep "< HTTP/1.1 200 OK" curl_onlyifcached_postitive_out
85
+ '
86
+ test_expect_success "GET for /ipfs/ with only-if-cached fails when not in local datastore" '
87
+ curl -svX GET -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$(date | ipfs add --only-hash -Q)" >/dev/null 2>curl_onlyifcached_negative_out &&
88
+ cat curl_onlyifcached_negative_out &&
89
+ grep "< HTTP/1.1 412 Precondition Failed" curl_onlyifcached_negative_out
90
+ '
91
+
92
# X-Ipfs-Path
93
94
## dir generated listing