Menu

#3288 Audio/Video element with preload can lead to precaching getting a 206 response and crashing

open
nobody
None
2024-11-22
2024-01-11
Anonymous
No

Originally created by: segevfiner

Library Affected:
workbox-precaching

Browser & Platform:
Google Chrome 120.0.6099.199 Desktop

Issue or Feature Request Description:
On a page with an audio/video elements with preload="auto", where workbox is also set to precache the audio/video src file, sometimes the service worker ends up getting a 206 in its install event for its precache request, which leads to the following error:

workbox-0f22832a.js:2064 Uncaught (in promise) TypeError: Failed to execute 'put' on 'Cache': Partial response (status code 206) is unsupported
    at StrategyHandler.cachePut (workbox-0f22832a.js:2064:23)
    at async PrecacheStrategy._handleInstall (workbox-0f22832a.js:3072:27)
    at async PrecacheStrategy._handle (workbox-0f22832a.js:3005:18)
    at async PrecacheStrategy._getResponse (workbox-0f22832a.js:2402:22)
cachePut @ workbox-0f22832a.js:2064

I'd assume it's taking the request to preload from the browser which has a Range request, before the pre-caching is done, and sending it to the network, trying to pre-cache based on it, instead of simply dropping the uncachable partial response, and still sending a normal pre-cache request separately.

I did follow https://developer.chrome.com/docs/workbox/serving-cached-audio-and-video (P.S. I guess the cacheableResponse plugin for this is redundant nowadays?)

When reporting bugs, please include relevant JavaScript Console logs and links to public URLs at which the issue could be reproduced.
The site is private, I will create a standalone repro later on.

Discussion

  • Anonymous

    Anonymous - 2024-01-11

    Originally posted by: segevfiner

    Hmm, actually I probably need to add the RangeRequests plugin to the precaching handler... So that runtimeCachine example from the linked article isn't helpful for that...

     
  • Anonymous

    Anonymous - 2024-01-11

    Originally posted by: segevfiner

    Doesn't seem to help even if I do that though...

     
  • Anonymous

    Anonymous - 2024-01-11

    Originally posted by: segevfiner

    I guess a workaround will be to do preload="none" and hope the user doesn't start any media before pre-caching ends... There doesn't seem to be an obvious way to add plugins to workbox-precaching in generateSW mode of workbox-build which I opened a separate request for https://github.com/GoogleChrome/workbox/issues/3289.

    I guess a fix will be to just let such requests through to the network, without trying to precache from their response, letting the normal pre-caching still take place, or alternatively to outright to dump the Range header when getting a request that has it during the install phase in the PrecachingStrategy, and maybe also pass the response through plugins so the ranges plugin can modify the complete response to only return the requested range even in that case.

     
  • Anonymous

    Anonymous - 2024-01-31

    Originally posted by: segevfiner

    Maybe we want something like this in the range requests plugin or inside workbox-precaching:

    // requestWillFetch() if from a plugin
        if (handler.event && handler.event.type === 'install') {
          request.headers.delete("Range")
        }
    
     
  • Anonymous

    Anonymous - 2024-01-31

    Originally posted by: segevfiner

    Doesn't work. The headers received in the request object seem empty for this case...

     
  • Anonymous

    Anonymous - 2024-01-31

    Originally posted by: segevfiner

    Doesn't help, still leads to bad-precaching-response.

     
  • Anonymous

    Anonymous - 2024-01-31

    Originally posted by: segevfiner

    So basically the browser sets a Range: bytes=0- header with preload="auto" and gets a full response with a 206 status, when the service worker starts after that request is complete, and tries to prefetch the file, the browser returns the 206 full content response (Racy, due to chromium coalescing the requests) which fails to be inserted into the cache with the described error. Something like this can possibly fix this:

        async cacheWillUpdate({ response }) {
          // The default will update logic of workbox-precaching
          if (!response || response.status >= 400) {
            return null;
          }
    
          if (response.status === 206 /* && fullBody */) {
            console.log('Patching 206 response');
            response = await copyResponse(response, (responseInit) => {
              responseInit.status = 200;
              return responseInit;
            })
          }
    
          return response;
        },
    

    But I think copyResponse will fail if the request is cross origin...

     
  • Anonymous

    Anonymous - 2024-11-21

    Originally posted by: piotr-cz

    @segevfiner did you found solution to this issue?

    Perhaps a plugin such as this one would help: https://github.com/GoogleChrome/workbox/issues/1644#issuecomment-1126871851

     
  • Anonymous

    Anonymous - 2024-11-21

    Originally posted by: segevfiner

    @segevfiner did you found solution to this issue?

    Perhaps a plugin such as this one would help: #1644 (comment)

    This is something I have in some experimental branch that I didn't deploy in the end (For injectManifest mode, as I can't seem to add precaching plugins in generateSW mode).

    Probably needs to add the full body check that it's missing to guard agains't mistakes, though I'm not sure if any partial 206 can reach it though.

    /// <reference lib="WebWorker" />
    declare let self: ServiceWorkerGlobalScope;
    
    import { clientsClaim, copyResponse } from 'workbox-core';
    import {
      addPlugins,
      cleanupOutdatedCaches,
      createHandlerBoundToURL,
      precacheAndRoute,
    } from 'workbox-precaching';
    import { RangeRequestsPlugin } from 'workbox-range-requests';
    import { NavigationRoute, registerRoute } from 'workbox-routing';
    
    void self.skipWaiting();
    clientsClaim();
    
    addPlugins([
      {
        // async requestWillFetch({ request, event }) {
        //   if (event && event.type === 'install') {
        //     console.log(request);
        //   }
        //   return request;
        // },
        async cacheWillUpdate({ response }) {
          if (!response || response.status >= 400) {
            return null;
          }
    
          if (response.status === 206 /* && fullBody */) {
            console.log('Patching 206 response');
            response = await copyResponse(response, (responseInit) => {
              responseInit.status = 200;
              return responseInit;
            });
          }
    
          return response;
        },
      },
      new RangeRequestsPlugin(),
    ]);
    precacheAndRoute(self.__WB_MANIFEST);
    cleanupOutdatedCaches();
    
    let allowlist: undefined | RegExp[];
    if (import.meta.env.DEV) {
      allowlist = [/^\/$/];
    }
    
    registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { allowlist }));
    
     
  • Anonymous

    Anonymous - 2024-11-22

    Originally posted by: piotr-cz

    Thanks, this seems to work (btw: videos Cache storage Status Code is 200 Partial Content and Response-Type is default instead of basic)

    Slightly different version of similar custom plugin can be found here: https://github.com/GoogleChrome/workbox/issues/1644#issuecomment-1126871851

     
  • Anonymous

    Anonymous - 2024-11-22

    Originally posted by: piotr-cz

    Here's is my take, which mixes your plugin (copyResponse usage) with this one: https://github.com/GoogleChrome/workbox/issues/1644#issuecomment-1126871851

    I'm not sure what is the best thing to return in case when conditions for partial response not are met (void | Response | null | undefined) so I've decided to return input and let workbox handle rest.

    // sw.ts
    import { copyResponse } from 'workbox-core'
    import { addPlugins } from 'workbox-precaching'
    import { RangeRequestsPlugin } from 'workbox-range-requests'
    
    addPlugins([
      {
        async cacheWillUpdate({ response }) {
          // Content encoding shouldn't be set, or content-type will be inaccurate
          if (response.status === 206 && response.headers.has('content-length') && !response.headers.has('content-encoding')) {
            const contentLength = Number.parseInt(response.headers.get('content-length')!)
    
            if (response.headers.get('content-range') === `bytes 0-${contentLength - 1}/${contentLength}`) {
              // Note: response still has the content-range header, but it should be ignored by browser for status code 200
              return copyResponse(response, (responseInit) => ({
                ...responseInit,
                status: 200,
                statusText: undefined,
              }))
            }
          }
    
          return response
        },
      },
      new RangeRequestsPlugin(),
    ])
    
     
  • Anonymous

    Anonymous - 2024-11-22

    Originally posted by: piotr-cz

    To reproduce the issue:

    1. add <video src="/assets/<some-video>.mp4" /> element to your app.
    2. use serve (npx serve -p 4173 dist)
    3. browser will request video with HTTP range requests and receive response with 206 Partial Content
    4. service worker will install, populate runtime cache for which browser will reuse above request
    5. browser will throw an error as in issue description
     

Log in to post a comment.