From 6fcbba4b4af18cd64aa253eff8df91a5ff26f801 Mon Sep 17 00:00:00 2001 From: Guillaume Michel Date: Fri, 19 Sep 2025 18:55:42 +0200 Subject: [PATCH] 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 --- config/routing.go | 54 ++++++++++++++++++++++++++++ core/commands/routing.go | 7 +++- core/node/provider.go | 6 ++++ test/cli/provider_test.go | 74 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 1 deletion(-) diff --git a/config/routing.go b/config/routing.go index bd234e8a3..d68016e4e 100644 --- a/config/routing.go +++ b/config/routing.go @@ -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 +} diff --git a/core/commands/routing.go b/core/commands/routing.go index e88b207d8..c772e2045 100644 --- a/core/commands/routing.go +++ b/core/commands/routing.go @@ -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() diff --git a/core/node/provider.go b/core/node/provider.go index b692fa8cd..3aff6e53c 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -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") } diff --git a/test/cli/provider_test.go b/test/cli/provider_test.go index 6617811f8..debeddcd0 100644 --- a/test/cli/provider_test.go +++ b/test/cli/provider_test.go @@ -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()