next-pwa: no matching service worker detected. you may need to reload the page or check that the scope of the service worker for the current page encloses the scope and start url from the manifest

Hello, I’ve been trying to setup next-pwa for the past two days but with no luck at all. Here’s my next config:

require('dotenv').config();
const path = require('path');
const Dotenv = require('dotenv-webpack');
const withPWA = require('next-pwa');
const withPlugins = require('next-compose-plugins');

const nextConfig = {
    webpack: (config, { isServer }) => {
        // Fixes npm packages that depend on `fs` module
        if (!isServer) {
            config.node = {
                fs: 'empty',
            };
        }
        // Read the .env file
        config.plugins = [
            ...config.plugins,
            new Dotenv({
                path: path.join(__dirname, process.env.ENV_FILE_PATH || '.env'),
                systemvars: true,
            }),
        ];
        return config;
    },
};

module.exports = withPlugins(
    [
        [
            withPWA,
            {
                pwa: {
                    dest: 'public',
                    sw: 'service-worker.js',
                    maximumFileSizeToCacheInBytes: 3000000,
                },
            },
        ],
    ],
    nextConfig
);

Next.js v9.2.1

server.js even though it's not required since v9 as mentioned in the docs, but I tried anyways

                if (
                    pathname === '/service-worker.js' ||
                    pathname.startsWith('/workbox-')
                ) {
                    const filePath = join(__dirname, '.next', pathname);
                    app.serveStatic(req, res, filePath);
                }

/public/manifest.json

{
  "name": "hello",
  "short_name": "hello",
  "theme_color": "#00a699",
  "background_color": "#ffffff",
  "display": "standalone",
  "orientation": "portrait",
  "scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "images/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "splash_pages": null
}

Any help is highly appreciated.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 30 (10 by maintainers)

Most upvoted comments

@LucasMallmann Sure thing, here you go(Although I shouldn’t do that):

const express = require('express');
const compression = require('compression');
const { parse } = require('url');
const { join } = require('path');
const dev = process.env.NODE_ENV !== 'production';
const next = require('next');
const app = next({ dev });
const handle = app.getRequestHandler();
const Cors = require('cors');
const Sentry = require('@sentry/node');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const helmet = require('helmet');
const {
    clearCompleteCache,
    clearCacheForRequestUrl,
    cacheManager,
} = require('./ssr-cacher')(app);
const { initMiddleware } = require('./util/init-middleware');
const s3Client = require('./clients/s3');
const routes = require('./routes');
const {
    port,
    hostName,
    hostNameRegex,
    appEnv,
    sentryDsnBackend,
} = require('./config/variables');
const pathMap = require('./util/pathMap');
const isSamePage = require('./util/isSamePage');

// Initialize the cors middleware
const cors = initMiddleware(
    // You can read more about the available options here: https://github.com/expressjs/cors#configuration-options
    appEnv === 'production'
        ? Cors({ origin: ['origin'] })
        : Cors({
            origin: hostNameRegex,
        })
);

/* this is used for server only, find the web version in pages => _app */
Sentry.init({
    dsn: `${sentryDsnBackend}`,
});

app.prepare()
    .then(() => {
        const server = express();
        server.use(Sentry.Handlers.requestHandler());
        server.use(helmet());
        server.use(compression());
        server.use(cookieParser());
        server.use(csrf({ cookie: true }));

        routes.map((route) =>
            server.get(route.path, async (req, res) => {
                if (req) {
                    await cors(req, res);
                }
                const parsedUrl = parse(req.url, true);
                const { pathname } = parsedUrl;
                if ('/properties/:itemUrl' === route.path) {
                    // if (dev || appEnv === 'staging' || req.query.noCache) {
                    res.setHeader('X-Cache-Status', 'DISABLED');
                    return app.render(req, res, route.page, {
                        ...req.params,
                        itemUrl: req.params.itemUrl,
                    });
                    // } else {
                    //     return cacheManager({
                    //         req,
                    //         res,
                    //         pagePath: route.page,
                    //         queryParams: {
                    //             ...req.params,
                    //             itemUrl: req.params.itemUrl,
                    //         },
                    //     });
                    // }
                }
                if (
                    '/search/:type' === route.path ||
                    '/search/:type/:cat1' === route.path ||
                    '/search/:type/:cat1/:cat2' === route.path ||
                    '/search/:type/:cat1/:cat2/:cat3' === route.path ||
                    '/search/:type/:cat1/:cat2/:cat3/:cat4' === route.path ||
                    '/search/:type/:area' === route.path ||
                    '/search/:type/:cat1/:area' === route.path ||
                    '/search/:type/:cat1/:cat2/:area' === route.path ||
                    '/search/:type/:cat1/:cat2/:cat3/:area' === route.path ||
                    '/search/:type/:cat1/:cat2/:cat3/:cat4/:area' === route.path
                ) {
                    if (dev || appEnv === 'staging' || req.query.noCache) {
                        res.setHeader('X-Cache-Status', 'DISABLED');
                        return app.render(req, res, route.page);
                    } else {
                        return cacheManager({
                            req,
                            res,
                            pagePath: route.page,
                        });
                    }
                }
                if ('/update-list/:id' === route.path) {
                    if (validateUser(req, res)) {
                        if (dev || appEnv === 'staging' || req.query.noCache) {
                            res.setHeader('X-Cache-Status', 'DISABLED');
                            return app.render(req, res, route.page, {
                                id: req.params.id,
                            });
                        } else {
                            return cacheManager({
                                req,
                                res,
                                pagePath: route.page,
                                queryParams: {
                                    ...req.params,
                                    id: req.params.id,
                                },
                            });
                        }
                    }
                }
                if ('/user-details/:id' === route.path) {
                    if (dev || appEnv === 'staging' || req.query.noCache) {
                        res.setHeader('X-Cache-Status', 'DISABLED');
                        return app.render(req, res, route.page, {
                            id: req.params.id,
                        });
                    } else {
                        return cacheManager({
                            req,
                            res,
                            pagePath: route.page,
                            queryParams: {
                                ...req.params,
                                id: req.params.id,
                            },
                        });
                    }
                }
            })
        );

        server.get('*/new-list', async (req, res) => {
            if (req) {
                await cors(req, res);
            }
            if (validateUser(req, res)) {
                if (dev || appEnv === 'staging' || req.query.noCache) {
                    res.setHeader('X-Cache-Status', 'DISABLED');
                    return handle(req, res);
                } else {
                    return cacheManager({ req, res, pagePath: req.path });
                }
            }
        });

        server.get('/service-worker.js', (request, response) => {
            const filePath = join(__dirname, 'public/service-worker.js');
            return app.serveStatic(request, response, filePath);
        });

        server.get(/workbox-[a-zA-Z0-9]+.js$/, async (request, response) => {
            const basePath = resolve(__dirname, 'public');
            const result = await FindFiles(basePath, /workbox-[a-zA-Z0-9]+.js$/);

            const [fileMatchResult] = result;
            const { dir, file } = fileMatchResult;
            const fileWithPathToServe = join(dir, file);

            return app.serveStatic(request, response, fileWithPathToServe);
        });

        server.get('*/user', async (req, res) => {
            if (req) {
                await cors(req, res);
            }
            if (validateUser(req, res)) {
                // if (dev || appEnv === 'staging' || req.query.noCache) {
                res.setHeader('X-Cache-Status', 'DISABLED');
                return handle(req, res);
                // }
                // else {
                //     return cacheManager({ req, res, pagePath: req.path });
                // }
            }
        });

        server.get('/sitemap-index.xml', async function (req, res) {
        });

        server.get(/sitemap\-[0-9]*\.xml/, async function (req, res) {
        });

        // Do not use caching for _next files
        server.get('/_next/*', (req, res) => {
            return handle(req, res);
        });

        // Do  use caching for files
        server.get('*', async (req, res) => {
            if (req) {
                await cors(req, res);
            }
            if (dev || appEnv === 'staging' || req.query.noCache) {
                res.setHeader('X-Cache-Status', 'DISABLED');
                if (req.url.startsWith('/public/')) {
                    return app.serveStatic(req, res, join(__dirname, req.url));
                }
                return handle(req, res);
            } else {
                if (req.url.startsWith('/public/')) {
                    return app.serveStatic(req, res, join(__dirname, req.url));
                }
                if (!pathMap.has(req.url)) {
                    for (let key of pathMap.keys()) {
                        if (req.url.startsWith(key)) {
                            if (isSamePage(key, req.url)) {
                                return cacheManager({
                                    req,
                                    res,
                                    pagePath: req.url,
                                });
                            } else {
                                return handle(req, res);
                            }
                        }
                    }
                    return handle(req, res);
                } else {
                    if (pathMap.get(req.url).startsWith(req.url)) {
                        return cacheManager({ req, res, pagePath: req.path });
                    } else {
                        return handle(req, res);
                    }
                }
            }
        });

        server.use(Sentry.Handlers.errorHandler());

        server.purge('*', (req, res) => {
            if (req.query.clearCache) {
                clearCompleteCache(res, req);
            } else {
                clearCacheForRequestUrl(req, res);
            }
        });

        server.listen(port, (err) => {
            if (err) throw err;
            console.log(`> Ready on http://localhost:${port}`);
        });
    })
    .catch((error) => {
        console.log('server.js => error', error);
        Sentry.captureException(error);
    });

function validateUser(req, res) {
    if (
        req.headers.cookie &&
        req.headers.cookie.indexOf('persist:user') > -1
    ) {
        return true;
    } else {
        res.redirect('/user/login');
    }
}

I had the same error message, “No matching service worker detected…” and I came to this topic, but I’m not using AMP and the middleware was causing the issue and this solution solved my me.

Basically, I added the runtimeCaching and buildExcludes to the PWA config inside the next.config.js.

I think you can set the scope for non-AMP service worker to / and change rules in runtimeCaching to exclude the AMP routes. That way it will not do anything on AMP pages.

@shadowwalker Hello, I just wanted to let you and any future developers who come across this issue know how I solved it. Since our website is half amp, half SSR, what I’ve done was creating two separate service workers, one for amp controlling amp pages only and one for the other pages.

AMP docs on how to install/register amp service worker: https://amp.dev/documentation/examples/components/amp-install-serviceworker/ working example provided by Nextjs: https://github.com/vercel/next.js/tree/canary/examples/amp-first

and this is the server endpoints as suggested by you guys:

 server.get('/service-worker.js', (req, res) => {
            const filePath = join(__dirname, 'public/service-worker.js');
            return app.serveStatic(req, res, filePath);
        });

    server.get(/worker-[a-zA-Z0-9]+.js$/, async (req, res) => {
        const { pathname } = parse(req.url, true);
        return app.serveStatic(
            req,
            res,
            join(__dirname, 'public', pathname)
        );
    });

    server.get(/workbox-[a-zA-Z0-9]+.js$/, async (req, res) => {
        const { pathname } = parse(req.url, true);
        return app.serveStatic(
            req,
            res,
            join(__dirname, 'public', pathname)
        );
    });

and server endpoint for amp service worker:

    server.get('/serviceworker-amp.js', (req, res) => {
        const filePath = join(__dirname, 'public/serviceworker-amp.js');
        return app.serveStatic(req, res, filePath);
    });

next-pwa config:

const withPWA = require('next-pwa');

const config = {
    pwa: {
        disable: process.env.NODE_ENV === 'development',
        register: true,
        scope: '/search',
        sw: 'service-worker.js',
        dest: 'public',
        maximumFileSizeToCacheInBytes: 3145728,
    },
};

module.exports = process.env.NODE_ENV === 'development' ? {} : withPWA(config);

As you can see, for the non-amp service worker I had to set the scope to a single page only which is the search page. Any ways I could make it handle all other pages that the amp service worker doesn’t handle? (amp controlls / and /properties)

@omar-dulaimi I understand it’s frustrating to debug those issue. I would assume AMP won’t work well with PWA, (will have to experiment this a bit later). Both technologies are trying to speed up page loads, but they are running in totally different direction. AMP pages will be cached on Google Server and will be served by Google to Visitors once cached. I assume for security reasons, those pages won’t work well with service worker. My suggestions here

  1. As long as non AMP pages work with PWA, moving on from this issue for now.
  2. For AMP pages, try to limit the page to only have contents, not put complex javascript on the page.

Hey @shadowwalker, I just tried that, and it still only works on Non AMP pages.

Hey @LucasMallmann I moved both handlers above, right below
server.use(csrf({ cookie: true })); But it still didn’t work.