fix: allow custom http provide when libp2p node is offline (#10974)

* feat: allow custom http provide when offline

* refactor: improve offline HTTP provider handling and tests

- fixed comment/function name mismatch
- added mock server test for HTTP provide success
- clarified test names for offline scenarios

* test: simplify single-node provider tests

use h.NewNode().Init() instead of NewNodes(1) for cleaner test setup

* fix: allow SweepingProvider to work with HTTP-only routing

when no DHT is available but HTTP routers are configured for providing,
return NoopProvider instead of failing. this allows the daemon to start
and HTTP-based providing to work through the routing system.

moved HTTP provider detection to config package as HasHTTPProviderConfigured()
for better code organization and reusability.

this fix is important as SweepingProvider will become the new default in the future.

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
This commit is contained in:
Guillaume Michel 2025-09-19 18:55:42 +02:00 committed by GitHub
parent 07f017f01d
commit 6fcbba4b4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 140 additions and 1 deletions

View File

@ -214,3 +214,57 @@ func getEnvOrDefault(key string, defaultValue []string) []string {
}
return defaultValue
}
// HasHTTPProviderConfigured checks if the node is configured to use HTTP routers
// for providing content announcements. This is used when determining if the node
// can provide content even when not connected to libp2p peers.
//
// Note: Right now we only support delegated HTTP content providing if Routing.Type=custom
// and Routing.Routers are configured according to:
// https://github.com/ipfs/kubo/blob/master/docs/delegated-routing.md#configuration-file-example
//
// This uses the `ProvideBitswap` request type that is not documented anywhere,
// because we hoped something like IPIP-378 (https://github.com/ipfs/specs/pull/378)
// would get finalized and we'd switch to that. It never happened due to politics,
// and now we are stuck with ProvideBitswap being the only API that works.
// Some people have reverse engineered it (example:
// https://discuss.ipfs.tech/t/only-peers-found-from-dht-seem-to-be-getting-used-as-relays-so-cant-use-http-routers/19545/9)
// and use it, so what we do here is the bare minimum to ensure their use case works
// using this old API until something better is available.
func (c *Config) HasHTTPProviderConfigured() bool {
if len(c.Routing.Routers) == 0 {
// No "custom" routers
return false
}
method, ok := c.Routing.Methods[MethodNameProvide]
if !ok {
// No provide method configured
return false
}
return c.routerSupportsHTTPProviding(method.RouterName)
}
// routerSupportsHTTPProviding checks if the supplied custom router is or
// includes an HTTP-based router.
func (c *Config) routerSupportsHTTPProviding(routerName string) bool {
rp, ok := c.Routing.Routers[routerName]
if !ok {
// Router configured for providing doesn't exist
return false
}
switch rp.Type {
case RouterTypeHTTP:
return true
case RouterTypeParallel, RouterTypeSequential:
// Check if any child router supports HTTP
if children, ok := rp.Parameters.(*ComposableRouterParams); ok {
for _, childRouter := range children.Routers {
if c.routerSupportsHTTPProviding(childRouter.RouterName) {
return true
}
}
}
}
return false
}

View File

@ -170,10 +170,15 @@ var provideRefRoutingCmd = &cmds.Command{
return errors.New("invalid configuration: Provide.Enabled is set to 'false'")
}
if len(nd.PeerHost.Network().Conns()) == 0 {
if len(nd.PeerHost.Network().Conns()) == 0 && !cfg.HasHTTPProviderConfigured() {
// Node is depending on DHT for providing (no custom HTTP provider
// configured) and currently has no connected peers.
return errors.New("cannot provide, no connected peers")
}
// If we reach here with no connections but HTTP provider configured,
// we proceed with the provide operation via HTTP
// Needed to parse stdin args.
// TODO: Lazy Load
err = req.ParseBodyArgs()

View File

@ -355,6 +355,12 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option {
}
}
if impl == nil {
// No DHT available, check if HTTP provider is configured
cfg, err := in.Repo.Config()
if err == nil && cfg.HasHTTPProviderConfigured() {
// HTTP provider is configured, return NoopProvider to allow HTTP-based providing
return &NoopProvider{}, keyStore, nil
}
return &NoopProvider{}, nil, errors.New("provider: no valid DHT available for providing")
}

View File

@ -3,6 +3,9 @@ package cli
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@ -139,6 +142,77 @@ func runProviderSuite(t *testing.T, reprovide bool, apply cfgApplier) {
expectNoProviders(t, cid, nodes[1:]...)
})
t.Run("manual provide fails when no libp2p peers and no custom HTTP router", func(t *testing.T) {
t.Parallel()
h := harness.NewT(t)
node := h.NewNode().Init()
apply(node)
node.SetIPFSConfig("Provide.Enabled", true)
node.StartDaemon()
defer node.StopDaemon()
cid := node.IPFSAddStr(time.Now().String())
res := node.RunIPFS("routing", "provide", cid)
assert.Contains(t, res.Stderr.Trimmed(), "cannot provide, no connected peers")
assert.Equal(t, 1, res.ExitCode())
})
t.Run("manual provide succeeds via custom HTTP router when no libp2p peers", func(t *testing.T) {
t.Parallel()
// Create a mock HTTP server that accepts provide requests.
// This simulates the undocumented API behavior described in
// https://discuss.ipfs.tech/t/only-peers-found-from-dht-seem-to-be-getting-used-as-relays-so-cant-use-http-routers/19545/9
// Note: This is NOT IPIP-378, which was not implemented.
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Accept both PUT and POST requests to /routing/v1/providers and /routing/v1/ipns
if (r.Method == http.MethodPut || r.Method == http.MethodPost) &&
(strings.HasPrefix(r.URL.Path, "/routing/v1/providers") || strings.HasPrefix(r.URL.Path, "/routing/v1/ipns")) {
// Return HTTP 200 to indicate successful publishing
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer mockServer.Close()
h := harness.NewT(t)
node := h.NewNode().Init()
apply(node)
node.SetIPFSConfig("Provide.Enabled", true)
// Configure a custom HTTP router for providing.
// Using our mock server that will accept the provide requests.
routingConf := map[string]any{
"Type": "custom", // https://github.com/ipfs/kubo/blob/master/docs/delegated-routing.md#configuration-file-example
"Methods": map[string]any{
"provide": map[string]any{"RouterName": "MyCustomRouter"},
"get-ipns": map[string]any{"RouterName": "MyCustomRouter"},
"put-ipns": map[string]any{"RouterName": "MyCustomRouter"},
"find-peers": map[string]any{"RouterName": "MyCustomRouter"},
"find-providers": map[string]any{"RouterName": "MyCustomRouter"},
},
"Routers": map[string]any{
"MyCustomRouter": map[string]any{
"Type": "http",
"Parameters": map[string]any{
// Use the mock server URL
"Endpoint": mockServer.URL,
},
},
},
}
node.SetIPFSConfig("Routing", routingConf)
node.StartDaemon()
defer node.StopDaemon()
cid := node.IPFSAddStr(time.Now().String())
// The command should successfully provide via HTTP even without libp2p peers
res := node.RunIPFS("routing", "provide", cid)
assert.Empty(t, res.Stderr.String(), "Should have no errors when providing via HTTP router")
assert.Equal(t, 0, res.ExitCode(), "Should succeed with exit code 0")
})
// Right now Provide and Reprovide are tied together
t.Run("Reprovide.Interval=0 disables announcement of new CID too", func(t *testing.T) {
t.Parallel()