**TL;DR**: When upgrading from `Bitcoin Core 0.20.1` to `Bitcoin Core 0.21.0`, a downstream application using the HTTP JSON-RPC API started getting HTTP 403 "Forbidden".

Turns out it is because I have `rpcallowip=::/0` in `bitcoin.conf` which used to work but does not anymore.
As a workaround, `rpcallowip=0.0.0.0/0` works.

-----

I run bitcoind in docker container1, and some application using the HTTP JSON-RPC API of bitcoind is in docker container2. They are on the same "docker network".

In `bitcoin.conf` I have:
```
rpcauth=<...>
rpcbind=0.0.0.0:8332
rpcallowip=::/0
```
(bitcoind RPC is not actually exposed to the public internet; it is up to the docker layer and above to ensure this.)

The application in container2 is connecting through HTTP accessing an URL such as `http://user:pass@172.19.1.10:8332/`.

This setup used to work when running `Bitcoin Core 0.20.1` in container1.

It no longer works with `Bitcoin Core 0.21.0`.

-----

If I change my `bitcoin.conf` as below, it starts working again using 0.21:
```
rpcauth=<...>
rpcbind=0.0.0.0:8332
rpcallowip=0.0.0.0/0
```

So `rpcallowip=::/0` used to work but does not anymore; `rpcallowip=0.0.0.0/0` works.

-----

Easy way to test:
`curl --data-binary '{"jsonrpc":"1.0","id":"curltext","method":"getblockchaininfo","params":[]}' -H 'content-type:text/plain;' http://user:pass@172.19.1.10:8332/`
options.reindex = node::fReindex;
        options.reindex_chainstate = fReindexChainState;
        options.prune = chainman.m_blockman.IsPruneMode();
        options.check_blocks = args.GetIntArg("-checkblocks", DEFAULT_CHECKBLOCKS);
        options.check_level = args.GetIntArg("-checklevel", DEFAULT_CHECKLEVEL);
        options.require_full_verification = args.IsArgSet("-checkblocks") || args.IsArgSet("-checklevel");
        options.check_interrupt = ShutdownRequested;
        options.coins_error_cb = [] {
            uiInterface.ThreadSafeMessageBox(
                _("Error reading from database, shutting down."),
                "", CClientUIInterface::MSG_ERROR);
        };
        uiInterface.InitMessage(_("Loading block index…").translated);
        const auto load_block_index_start_time{SteadyClock::now()};
        auto catch_exceptions = [](auto&& f) {
            try {
                return f();
            } catch (const std::exception& e) {
                LogPrintf("%s\n", e.what());
                return std::make_tuple(node::ChainstateLoadStatus::FAILURE, _("Error opening block database"));
            }
        };
        auto [status, error] = catch_exceptions([&]{ return LoadChainstate(chainman, cache_sizes, options); });
        if (status == node::ChainstateLoadStatus::SUCCESS) {
            uiInterface.InitMessage(_("Verifying blocks…").translated);
            if (chainman.m_blockman.m_have_pruned && options.check_blocks > MIN_BLOCKS_TO_KEEP) {
                LogPrintfCategory(BCLog::PRUNE, "pruned datadir may not have more than %d blocks; only checking available blocks\n",
                                  MIN_BLOCKS_TO_KEEP);
            }
            std::tie(status, error) = catch_exceptions([&]{ return VerifyLoadedChainstate(chainman, options);});