diff --git a/fsthttp/cache.go b/fsthttp/cache.go index 1da242f..018f64f 100644 --- a/fsthttp/cache.go +++ b/fsthttp/cache.go @@ -27,6 +27,9 @@ type CandidateResponse struct { overrideStaleWhileRevalidate uint32 // seconds useSWR bool + overrideStaleIfError uint32 // seconds + useSIE bool + extraSurrogateKeys string overrideSurrogateKeys string useSurrogate bool @@ -47,15 +50,16 @@ type cacheResponse struct { } type cacheWriteOptions struct { - maxAge uint32 // seconds - vary string - useVary bool - age uint32 // seconds - stale uint32 // seconds - surrogate string - length uint64 - useLength bool - sensitive bool + maxAge uint32 // seconds + vary string + useVary bool + age uint32 // seconds + staleRevalidate uint32 // seconds + surrogate string + length uint64 + useLength bool + sensitive bool + staleError uint32 abiOpts fastly.HTTPCacheWriteOptions } @@ -69,9 +73,10 @@ func (opts *cacheWriteOptions) flushToABI() { opts.abiOpts.SetMaxAgeNs(u32sTou64ns(opts.maxAge)) opts.abiOpts.SetVaryRule(opts.vary) opts.abiOpts.SetInitialAgeNs(u32sTou64ns(opts.age)) - opts.abiOpts.SetStaleWhileRevalidateNs(u32sTou64ns(opts.stale)) + opts.abiOpts.SetStaleWhileRevalidateNs(u32sTou64ns(opts.staleRevalidate)) opts.abiOpts.SetSurrogateKeys(opts.surrogate) opts.abiOpts.SetSensitiveData(opts.sensitive) + opts.abiOpts.SetStaleIfErrorNs(u32sTou64ns(opts.staleError)) } func (opts *cacheWriteOptions) loadFromABI() { @@ -81,11 +86,15 @@ func (opts *cacheWriteOptions) loadFromABI() { opts.age = u64nsTou32s(ns) } if ns, ok := opts.abiOpts.StaleWhileRevalidateNs(); ok { - opts.stale = u64nsTou32s(ns) + opts.staleRevalidate = u64nsTou32s(ns) } opts.surrogate, _ = opts.abiOpts.SurrogateKeys() opts.length, opts.useLength = opts.abiOpts.Length() opts.sensitive = opts.abiOpts.SensitiveData() + + if ns, ok := opts.abiOpts.StaleIfErrorNs(); ok { + opts.staleError = u64nsTou32s(ns) + } } func (opts *cacheWriteOptions) loadFromHandle(c *fastly.HTTPCacheHandle) error { @@ -111,7 +120,7 @@ func (opts *cacheWriteOptions) loadFromHandle(c *fastly.HTTPCacheHandle) error { if ns, err := fastly.HTTPCacheGetStaleWhileRevalidateNs(c); err != nil { return fmt.Errorf("get stale while revalidate: %w", err) } else { - opts.stale = u64nsTou32s(uint64(ns)) + opts.staleRevalidate = u64nsTou32s(uint64(ns)) } opts.surrogate, err = fastly.HTTPCacheGetSurrogateKeys(c) @@ -136,6 +145,12 @@ func (opts *cacheWriteOptions) loadFromHandle(c *fastly.HTTPCacheHandle) error { return fmt.Errorf("get sensitive data: %w", err) } + if ns, err := fastly.HTTPCacheGetStaleIfErrorNs(c); err != nil { + return fmt.Errorf("get stale if error: %w", err) + } else { + opts.staleError = u64nsTou32s(uint64(ns)) + } + return nil } @@ -144,24 +159,15 @@ const ( cacheStorageActionInvalid = 0xffff ) -func httpCacheWait(c *fastly.HTTPCacheHandle) error { - _, err := fastly.HTTPCacheGetState(c) - if err != nil { - return fmt.Errorf("get state: %w", err) - } - return nil -} - -func httpCacheMustInsertOrUpdate(c *fastly.HTTPCacheHandle) (bool, error) { +func httpCacheWait(c *fastly.HTTPCacheHandle) (fastly.CacheLookupState, error) { state, err := fastly.HTTPCacheGetState(c) if err != nil { - return false, fmt.Errorf("get state: %w", err) - + return 0, fmt.Errorf("get state: %w", err) } - return state&fastly.CacheLookupStateMustInsertOrUpdate == fastly.CacheLookupStateMustInsertOrUpdate, nil + return state, nil } -func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend string, transformForClient bool) (*Response, error) { +func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend string, transformForClient bool, wasHit bool) (*Response, error) { abiResp, abiBody, err := fastly.HTTPCacheGetFoundResponse(c, transformForClient) if err != nil { if status, ok := fastly.IsFastlyError(err); ok && status == fastly.FastlyStatusNone { @@ -170,9 +176,13 @@ func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend return nil, fmt.Errorf("get found response: %w", err) } - hits, err := fastly.HTTPCacheGetHits(c) - if err != nil { - return nil, fmt.Errorf("get hits: %w", err) + var hits uint64 + if wasHit { + h, err := fastly.HTTPCacheGetHits(c) + if err != nil { + return nil, fmt.Errorf("get hits: %w", err) + } + hits = uint64(h) } var opts cacheWriteOptions @@ -189,7 +199,7 @@ func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend resp.cacheResponse = cacheResponse{ cacheWriteOptions: opts, storageAction: cacheStorageActionInvalid, - hits: uint64(hits), + hits: hits, } return resp, nil } @@ -239,6 +249,7 @@ func newCandidate(c *fastly.HTTPCacheHandle, opts *CacheOptions, abiResp *fastly overrideStorageAction: 0, overridePCI: opts.PCI, overrideStaleWhileRevalidate: opts.StaleWhileRevalidate, + overrideStaleIfError: opts.StaleIfError, extraSurrogateKeys: opts.SurrogateKey, overrideSurrogateKeys: "", overrideTTL: opts.TTL, @@ -253,6 +264,10 @@ func newCandidate(c *fastly.HTTPCacheHandle, opts *CacheOptions, abiResp *fastly candidate.useSWR = true } + if candidate.overrideStaleIfError != 0 { + candidate.useSIE = true + } + if candidate.overridePCI { candidate.usePCI = true } @@ -403,7 +418,40 @@ func (candidateResponse *CandidateResponse) StaleWhileRevalidate() (uint32, erro if err != nil { return 0, err } - return opts.stale, nil + return opts.staleRevalidate, nil +} + +// SetStaleWhileRevalidate sets the time in seconds for which a cached item can be delivered stale if synchronous revalidation produces an error. +func (candidateResponse *CandidateResponse) SetStaleIfError(sie uint32) { + candidateResponse.overrideStaleIfError = sie + candidateResponse.useSIE = true +} + +// StaleIfError returns the time in seconds for which a cached item be delivered stale if synchronous revalidation produces an error. +func (candidateResponse *CandidateResponse) StaleIfError() (uint32, error) { + if candidateResponse.useSIE { + return candidateResponse.overrideStaleIfError, nil + } + opts, err := candidateResponse.getSuggestedCacheWriteOptions() + if err != nil { + return 0, err + } + return opts.staleError, nil +} + +// Returns whether there is a stale-if-error response available from the cache. +// +// A CandidateResponse represents an HTTP response returned from a Backend. However, it may be +// preferable to return a cached response rather than the Backend's response -- for instance, +// if the Backend's response is a 5xx error. +// +// This method returns true if there is a cached response that is within the stale-if-error +// period. If a stale-if-error response is available, and the after_send hook returns an +// error, the response from the Backend will not be cached, and the [Request::send] call will +// return the stale-if-error response. +func (candidateResponse *CandidateResponse) StaleIfErrorAvailable() bool { + state, _ := fastly.HTTPCacheGetState(candidateResponse.cacheHandle) + return state&fastly.CacheLookupStateUsableIfError == fastly.CacheLookupStateUsableIfError } // SetSensitive sets the caching behavior of this response to enable or disable PCI/HIPAA-compliant @@ -582,9 +630,9 @@ func (candidateResponse *CandidateResponse) finalizeOptions() (fastly.HTTPCacheS opts.age = suggestedCacheWriteOptions.age if candidateResponse.useSWR { - opts.stale = candidateResponse.overrideStaleWhileRevalidate + opts.staleRevalidate = candidateResponse.overrideStaleWhileRevalidate } else { - opts.stale = suggestedCacheWriteOptions.stale + opts.staleRevalidate = suggestedCacheWriteOptions.staleRevalidate } if candidateResponse.useVary { @@ -657,7 +705,7 @@ func (candidateResponse *CandidateResponse) applyAndStreamBack(req *Request) (*R } body.Close() - resp, err = httpCacheGetFoundResponse(readback, req, "", false) + resp, err = httpCacheGetFoundResponse(readback, req, "", false, true) if err != nil { return nil, fmt.Errorf("cache get found response: %w", err) } @@ -669,7 +717,7 @@ func (candidateResponse *CandidateResponse) applyAndStreamBack(req *Request) (*R } defer fastly.HTTPCacheTransactionClose(newch) - resp, err = httpCacheGetFoundResponse(newch, req, "", true) + resp, err = httpCacheGetFoundResponse(newch, req, "", true, true) if err != nil { return nil, fmt.Errorf("cache get found response: %w", err) } diff --git a/fsthttp/request.go b/fsthttp/request.go index 292bde7..cbe50fa 100644 --- a/fsthttp/request.go +++ b/fsthttp/request.go @@ -635,12 +635,13 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re fastly.HTTPCacheTransactionClose(cacheHandle) } }() - if err := httpCacheWait(cacheHandle); err != nil { + state, err := httpCacheWait(cacheHandle) + if err != nil { return nil, err } // is there a "usable" cached response (i.e. fresh or within SWR period) - resp, err := httpCacheGetFoundResponse(cacheHandle, req, backend, true) + resp, err := httpCacheGetFoundResponse(cacheHandle, req, backend, true, true) if err != nil { return nil, err } @@ -650,7 +651,7 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re // if this is during SWR, we may be the "lucky winner" who is // tasked with performing a background revalidation - if ok, _ := httpCacheMustInsertOrUpdate(cacheHandle); ok { + if state.Has(fastly.CacheLookupStateMustInsertOrUpdate) { pending, err := req.sendAsyncForCaching(ctx, cacheHandle, backend) if err != nil { return nil, err @@ -672,7 +673,14 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re cacheHandle = nil } - // Meanwhile, whether fresh or in SWR, we can immediately return + if state.Has(fastly.CacheLookupStateUsableIfError) { + // This is a stale-if-error response that is also USABLE, implying the request + // collapse has already happened. + // Mark the response's masked error as "error in request collapse leader". + resp.maskedError = ErrRequestCollapse + } + + // Meanwhile, whether fresh or in SWR/SIE, we can immediately return // the cached response: resp.updateFastlyCacheHeaders(req) return resp, nil @@ -680,8 +688,7 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re // no cached response - if ok, _ := httpCacheMustInsertOrUpdate(cacheHandle); ok { - + if state.Has(fastly.CacheLookupStateMustInsertOrUpdate) { pending, err := req.sendAsyncForCaching(ctx, cacheHandle, backend) if err != nil { return nil, err @@ -689,6 +696,19 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re candidateResp, err := newCandidateFromPendingBackendCaching(pending) if err != nil { + if state.Has(fastly.CacheLookupStateUsableIfError) { + // Substitute stale-if-error response; let anyone else in the collapse know as + // well. + fastly.HTTPCacheTransactionChooseStale(cacheHandle) + resp, foundErr := httpCacheGetFoundResponse(cacheHandle, req, backend, true, false) + if foundErr != nil { + return nil, foundErr + } + resp.maskedError = err + resp.updateFastlyCacheHeaders(req) + return resp, nil + } + return nil, err } @@ -998,6 +1018,12 @@ type CacheOptions struct { // bypass the cache. StaleWhileRevalidate uint32 + // The maximum duration after `max_age` during which the response may be delivered stale + // if synchronous revalidation produces an error. + // + // If this field is not set, the default value is zero. + StaleIfError uint32 + // SurrogateKey represents an explicit surrogate key for the request, which // will be added to any `Surrogate-Key` response headers received from the // backend. If nonempty, the request will not be forced to bypass the cache. diff --git a/fsthttp/response.go b/fsthttp/response.go index 7598507..7959bd7 100644 --- a/fsthttp/response.go +++ b/fsthttp/response.go @@ -36,6 +36,10 @@ type Response struct { trailers Header + // If this response was served from the cache *and* the response was stale-if-error, + // this is the error from the revalidation attempt. + maskedError error + cacheResponse cacheResponse abi struct { @@ -113,6 +117,12 @@ func (resp *Response) Trailers() (Header, error) { return resp.trailers, nil } +// If this response was served from the cache *and* the response was stale-if-error, +// this is the error from the revalidation attempt. +func (r *Response) MaskedError() error { + return r.maskedError +} + type netaddr struct { ip net.IP port uint16 @@ -236,7 +246,16 @@ func (resp *Response) StaleWhileRevalidate() (uint32, bool) { return 0, false } - return resp.cacheResponse.cacheWriteOptions.stale, true + return resp.cacheResponse.cacheWriteOptions.staleRevalidate, true +} + +// StaleIfError returns the time in seconds for which a cached item delivered stale if synchronous revalidation produces an error. +func (resp *Response) StaleIfError() (uint32, bool) { + if resp.wasWrittenToCache() { + return 0, false + } + + return resp.cacheResponse.cacheWriteOptions.staleError, true } // Vary returns the set of request headers for which the response may vary. diff --git a/fsthttp/senderror.go b/fsthttp/senderror.go index f46a3e3..226c151 100644 --- a/fsthttp/senderror.go +++ b/fsthttp/senderror.go @@ -2,7 +2,11 @@ package fsthttp -import "github.com/fastly/compute-sdk-go/internal/abi/fastly" +import ( + "errors" + + "github.com/fastly/compute-sdk-go/internal/abi/fastly" +) // SendError provides detailed information about backend request failures. // @@ -131,3 +135,7 @@ const ( // error. SendErrorInternalError = fastly.SendErrorDetailTagInternalError ) + +var ( + ErrRequestCollapse = errors.New("error during request collapse") +) diff --git a/internal/abi/fastly/hostcalls_noguest.go b/internal/abi/fastly/hostcalls_noguest.go index 247daa0..2ab781d 100644 --- a/internal/abi/fastly/hostcalls_noguest.go +++ b/internal/abi/fastly/hostcalls_noguest.go @@ -753,6 +753,10 @@ func (o *HTTPCacheWriteOptions) SetSensitiveData(sensitive bool) {} func (o *HTTPCacheWriteOptions) SensitiveData() bool { return false } +func (o *HTTPCacheWriteOptions) SetStaleIfErrorNs(staleIfErrorNs uint64) {} + +func (o *HTTPCacheWriteOptions) StaleIfErrorNs() (uint64, bool) { return 0, false } + func HTTPCacheTransactionInsert(h *HTTPCacheHandle, resp *HTTPResponse, opts *HTTPCacheWriteOptions) (*HTTPBody, error) { return nil, fmt.Errorf("not implemented") } @@ -769,6 +773,10 @@ func HTTPCacheTransactionUpdateAndReturnFresh(h *HTTPCacheHandle, resp *HTTPResp return nil, fmt.Errorf("not implemented") } +func HTTPCacheTransactionChooseStale(h *HTTPCacheHandle) error { + return fmt.Errorf("not implemented") +} + func HTTPCacheTransactionRecordNotCacheable(h *HTTPCacheHandle, opts *HTTPCacheWriteOptions) error { return fmt.Errorf("not implemented") } @@ -813,6 +821,10 @@ func HTTPCacheGetStaleWhileRevalidateNs(h *HTTPCacheHandle) (httpCacheDurationNs return 0, fmt.Errorf("not implemented") } +func HTTPCacheGetStaleIfErrorNs(h *HTTPCacheHandle) (httpCacheDurationNs, error) { + return 0, fmt.Errorf("not implemented") +} + func HTTPCacheGetAgeNs(h *HTTPCacheHandle) (httpCacheDurationNs, error) { return 0, fmt.Errorf("not implemented") } diff --git a/internal/abi/fastly/httpcache_guest.go b/internal/abi/fastly/httpcache_guest.go index 71d7fc4..2c6b14b 100644 --- a/internal/abi/fastly/httpcache_guest.go +++ b/internal/abi/fastly/httpcache_guest.go @@ -115,6 +115,15 @@ func (o *HTTPCacheWriteOptions) SensitiveData() bool { return o.mask&httpCacheWriteOptionsFlagSensitiveData == httpCacheWriteOptionsFlagSensitiveData } +func (o *HTTPCacheWriteOptions) SetStaleIfErrorNs(staleNs uint64) { + o.opts.staleIfErrorNs = httpCacheDurationNs(staleNs) + o.mask |= httpCacheWriteOptionsFlagStaleIfError +} + +func (o *HTTPCacheWriteOptions) StaleIfErrorNs() (uint64, bool) { + return uint64(o.opts.staleIfErrorNs), o.mask&httpCacheWriteOptionsFlagStaleIfError == httpCacheWriteOptionsFlagStaleIfError +} + func (o *HTTPCacheWriteOptions) FillConfigMask() { o.mask = 0 | httpCacheWriteOptionsFlagReserved | @@ -123,7 +132,8 @@ func (o *HTTPCacheWriteOptions) FillConfigMask() { httpCacheWriteOptionsFlagStaleWhileRevalidate | httpCacheWriteOptionsFlagSurrogateKeys | httpCacheWriteOptionsFlagLength | - httpCacheWriteOptionsFlagSensitiveData + httpCacheWriteOptionsFlagSensitiveData | + httpCacheWriteOptionsFlagStaleIfError } // (module $fastly_http_cache @@ -427,6 +437,42 @@ func HTTPCacheTransactionUpdateAndReturnFresh(h *HTTPCacheHandle, resp *HTTPResp return &HTTPCacheHandle{h: newh}, nil } +// witx: +// +// ;;; Fulfill an obligation to provide a response to the cache by selecting a stale-if-error response. +// ;;; +// ;;; A guest that is obligated to insert/update the cache may not be able to produce an acceptable +// ;;; response (e.g. unreachable backend, 5xx response). If the cache contains a response in the +// ;;; stale-if-error period, the guest may prefer to use that response rather than returning an error. +// ;;; +// ;;; `transaction_choose_stale` is an alternative to `transaction_update_and_return_fresh` or +// ;;; `transaction_insert_and_stream_back`. Like those methods, it completes a request collapse, +// ;;; providing the stale response to all collapsed transactions; and, after calling +// ;;; `transaction_choose_stale`, the cache handle provides the (stale) response to send to the client. +// ;;; +// ;;; However, `transaction_choose_stale` does not change the cached state. The next lookup will again +// ;;; collapse and/or get an obligation to revalidate. +// (@interface func (export "transaction_choose_stale") +// (param $handle $http_cache_handle) +// (result $err (expected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_cache transaction_choose_stale +//go:noescape +func fastlyHTTPCacheTransactionChooseStale( + h httpCacheHandle, +) FastlyStatus + +func HTTPCacheTransactionChooseStale(h *HTTPCacheHandle) error { + if err := fastlyHTTPCacheTransactionChooseStale( + h.h, + ).toError(); err != nil { + return err + } + + return nil +} + // witx: // // ;;; Disable request collapsing and response caching for this cache entry. @@ -868,6 +914,35 @@ func HTTPCacheGetStaleWhileRevalidateNs(h *HTTPCacheHandle) (httpCacheDurationNs return d, nil } +// witx: +// +// ;;; Get the configured stale-if-error period of the found response in nanoseconds, +// ;;; returning the `$none` error if there was no response found. +// (@interface func (export "get_stale_if_error_ns") +// (param $handle $http_cache_handle) +// (result $err (expected $cache_duration_ns (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_cache get_stale_if_error_ns +//go:noescape +func fastlyHTTPCacheGetStaleIfErrorNs( + h httpCacheHandle, + d prim.Pointer[httpCacheDurationNs], +) FastlyStatus + +func HTTPCacheGetStaleIfErrorNs(h *HTTPCacheHandle) (httpCacheDurationNs, error) { + var d httpCacheDurationNs + + if err := fastlyHTTPCacheGetStaleIfErrorNs( + h.h, + prim.ToPointer(&d), + ).toError(); err != nil { + return 0, err + } + + return d, nil +} + // witx: // // ;;; Get the age of the found response in nanoseconds, returning the `$none` error if there was diff --git a/internal/abi/fastly/types.go b/internal/abi/fastly/types.go index b26adf3..0c2e191 100644 --- a/internal/abi/fastly/types.go +++ b/internal/abi/fastly/types.go @@ -992,8 +992,14 @@ const ( CacheLookupStateUsable CacheLookupState = 0b0000_0010 // $usable CacheLookupStateStale CacheLookupState = 0b0000_0100 // $stale CacheLookupStateMustInsertOrUpdate CacheLookupState = 0b0000_1000 // $must_insert_or_update + CacheLookupStateUsableIfError CacheLookupState = 0b0001_0000 // $usable_if_error + CacheLookupStateCollapseError CacheLookupState = 0b0010_0000 // $collapse_error ) +func (c CacheLookupState) Has(m CacheLookupState) bool { + return (c & m) == m +} + // witx: // // (typename $purge_options_mask @@ -1381,6 +1387,7 @@ const ( SendErrorDetailTagInternalError SendErrorDetailTag = 22 SendErrorDetailTagTLSAlertReceived SendErrorDetailTag = 23 SendErrorDetailTagTLSProtocolError SendErrorDetailTag = 24 + SendErrorDetailTagH2Error SendErrorDetailTag = 25 ) // witx: @@ -1404,6 +1411,7 @@ const ( sendErrorDetailMaskDNSErrorRCode = 1 << 1 // $dns_error_rcode sendErrorDetailMaskDNSErrorInfo = 1 << 2 // $dns_error_info_code sendErrorDetailMaskTLSAlertID = 1 << 3 // $tls_alert_id + sendErrorDetailMaskH2Error = 1 << 4 // $h2_error ) // witx: @@ -1415,6 +1423,8 @@ const ( // (field $dns_error_rcode u16) // (field $dns_error_info_code u16) // (field $tls_alert_id u8) +// (field $h2_error_frame u8) +// (field $h2_error_code u32) // )) // SendErrorDetail contains detailed error information from backend send operations. @@ -1424,6 +1434,8 @@ type SendErrorDetail struct { dnsErrorRCode prim.U16 dnsErrorInfoCode prim.U16 tlsAlertID prim.U8 + h2ErrorFrame prim.U8 + h2ErrorCode prim.U32 } func newSendErrorDetail() SendErrorDetail { @@ -1485,6 +1497,14 @@ func (d SendErrorDetail) TLSAlertID() uint8 { return uint8(d.tlsAlertID) } +func (d SendErrorDetail) H2ErrorFrame() uint8 { + return uint8(d.h2ErrorFrame) +} + +func (d SendErrorDetail) H2ErrorCode() uint32 { + return uint32(d.h2ErrorCode) +} + // TLSAlertDescription returns a human-readable description of the TLS alert. func (d SendErrorDetail) TLSAlertDescription() string { return tlsAlertString(d.tlsAlertID) @@ -1542,7 +1562,8 @@ func (d SendErrorDetail) String() string { return fmt.Sprintf("TLS alert received (%s)", tlsAlertString(d.tlsAlertID)) case SendErrorDetailTagTLSProtocolError: return "TLS protocol error" - + case SendErrorDetailTagH2Error: + return "HTTP/2 error" case SendErrorDetailTagUninitialized: panic("should not be reached: SendErrorDetailTagUninitialized") case SendErrorDetailTagOK: @@ -1795,6 +1816,12 @@ type httpCacheWriteOptions struct { // body have enough information to synthesize a `content-length` even before the complete // body is inserted to the cache. length httpCacheObjectLength + + // The maximum duration after `max_age` during which the response may be delivered stale + // if synchronous revalidation produces an error. + // + // If this field is not set, the default value is zero. + staleIfErrorNs httpCacheDurationNs } type httpCacheWriteOptionsMask prim.U32 @@ -1807,6 +1834,7 @@ const ( httpCacheWriteOptionsFlagSurrogateKeys httpCacheWriteOptionsMask = 1 << 4 httpCacheWriteOptionsFlagLength httpCacheWriteOptionsMask = 1 << 5 httpCacheWriteOptionsFlagSensitiveData httpCacheWriteOptionsMask = 1 << 6 + httpCacheWriteOptionsFlagStaleIfError httpCacheWriteOptionsMask = 1 << 7 ) // shielding.witx