@cryptotaxi247 / kubo / commits / 6fcbba4b4

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>

Guillaume Michel committed Sep 19, 2025 at 18:55 UTC 6fcbba4b4af18cd64aa253eff8df91a5ff26f801
4 files changed +140 -1
config/routing.go
+54
@@ -214,3 +214,57 @@ func getEnvOrDefault(key string, defaultValue []string) []string {
214 }
215 return defaultValue
216 }
217 +
218 +// HasHTTPProviderConfigured checks if the node is configured to use HTTP routers
219 +// for providing content announcements. This is used when determining if the node
220 +// can provide content even when not connected to libp2p peers.
221 +//
222 +// Note: Right now we only support delegated HTTP content providing if Routing.Type=custom
223 +// and Routing.Routers are configured according to:
224 +// https://github.com/ipfs/kubo/blob/master/docs/delegated-routing.md#configuration-file-example
225 +//
226 +// This uses the `ProvideBitswap` request type that is not documented anywhere,
227 +// because we hoped something like IPIP-378 (https://github.com/ipfs/specs/pull/378)
228 +// would get finalized and we'd switch to that. It never happened due to politics,
229 +// and now we are stuck with ProvideBitswap being the only API that works.
230 +// Some people have reverse engineered it (example:
231 +// https://discuss.ipfs.tech/t/only-peers-found-from-dht-seem-to-be-getting-used-as-relays-so-cant-use-http-routers/19545/9)
232 +// and use it, so what we do here is the bare minimum to ensure their use case works
233 +// using this old API until something better is available.
234 +func (c *Config) HasHTTPProviderConfigured() bool {
235 + if len(c.Routing.Routers) == 0 {
236 + // No "custom" routers
237 + return false
238 + }
239 + method, ok := c.Routing.Methods[MethodNameProvide]
240 + if !ok {
241 + // No provide method configured
242 + return false
243 + }
244 + return c.routerSupportsHTTPProviding(method.RouterName)
245 +}
246 +
247 +// routerSupportsHTTPProviding checks if the supplied custom router is or
248 +// includes an HTTP-based router.
249 +func (c *Config) routerSupportsHTTPProviding(routerName string) bool {
250 + rp, ok := c.Routing.Routers[routerName]
251 + if !ok {
252 + // Router configured for providing doesn't exist
253 + return false
254 + }
255 +
256 + switch rp.Type {
257 + case RouterTypeHTTP:
258 + return true
259 + case RouterTypeParallel, RouterTypeSequential:
260 + // Check if any child router supports HTTP
261 + if children, ok := rp.Parameters.(*ComposableRouterParams); ok {
262 + for _, childRouter := range children.Routers {
263 + if c.routerSupportsHTTPProviding(childRouter.RouterName) {
264 + return true
265 + }
266 + }
267 + }
268 + }
269 + return false
270 +}
core/commands/routing.go
+6 -1
@@ -170,10 +170,15 @@ var provideRefRoutingCmd = &cmds.Command{
170 return errors.New("invalid configuration: Provide.Enabled is set to 'false'")
171 }
172
173 - if len(nd.PeerHost.Network().Conns()) == 0 {
173 + if len(nd.PeerHost.Network().Conns()) == 0 && !cfg.HasHTTPProviderConfigured() {
174 + // Node is depending on DHT for providing (no custom HTTP provider
175 + // configured) and currently has no connected peers.
176 return errors.New("cannot provide, no connected peers")
177 }
178
179 + // If we reach here with no connections but HTTP provider configured,
180 + // we proceed with the provide operation via HTTP
181 +
182 // Needed to parse stdin args.
183 // TODO: Lazy Load
184 err = req.ParseBodyArgs()
core/node/provider.go
+6
@@ -355,6 +355,12 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option {
355 }
356 }
357 if impl == nil {
358 + // No DHT available, check if HTTP provider is configured
359 + cfg, err := in.Repo.Config()
360 + if err == nil && cfg.HasHTTPProviderConfigured() {
361 + // HTTP provider is configured, return NoopProvider to allow HTTP-based providing
362 + return &NoopProvider{}, keyStore, nil
363 + }
364 return &NoopProvider{}, nil, errors.New("provider: no valid DHT available for providing")
365 }
366
test/cli/provider_test.go
+74
@@ -3,6 +3,9 @@ package cli
3 import (
4 "bytes"
5 "encoding/json"
6 + "net/http"
7 + "net/http/httptest"
8 + "strings"
9 "testing"
10 "time"
11
@@ -139,6 +142,77 @@ func runProviderSuite(t *testing.T, reprovide bool, apply cfgApplier) {
142 expectNoProviders(t, cid, nodes[1:]...)
143 })
144
145 + t.Run("manual provide fails when no libp2p peers and no custom HTTP router", func(t *testing.T) {
146 + t.Parallel()
147 +
148 + h := harness.NewT(t)
149 + node := h.NewNode().Init()
150 + apply(node)
151 + node.SetIPFSConfig("Provide.Enabled", true)
152 + node.StartDaemon()
153 + defer node.StopDaemon()
154 +
155 + cid := node.IPFSAddStr(time.Now().String())
156 + res := node.RunIPFS("routing", "provide", cid)
157 + assert.Contains(t, res.Stderr.Trimmed(), "cannot provide, no connected peers")
158 + assert.Equal(t, 1, res.ExitCode())
159 + })
160 +
161 + t.Run("manual provide succeeds via custom HTTP router when no libp2p peers", func(t *testing.T) {
162 + t.Parallel()
163 +
164 + // Create a mock HTTP server that accepts provide requests.
165 + // This simulates the undocumented API behavior described in
166 + // https://discuss.ipfs.tech/t/only-peers-found-from-dht-seem-to-be-getting-used-as-relays-so-cant-use-http-routers/19545/9
167 + // Note: This is NOT IPIP-378, which was not implemented.
168 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
169 + // Accept both PUT and POST requests to /routing/v1/providers and /routing/v1/ipns
170 + if (r.Method == http.MethodPut || r.Method == http.MethodPost) &&
171 + (strings.HasPrefix(r.URL.Path, "/routing/v1/providers") || strings.HasPrefix(r.URL.Path, "/routing/v1/ipns")) {
172 + // Return HTTP 200 to indicate successful publishing
173 + w.WriteHeader(http.StatusOK)
174 + } else {
175 + w.WriteHeader(http.StatusNotFound)
176 + }
177 + }))
178 + defer mockServer.Close()
179 +
180 + h := harness.NewT(t)
181 + node := h.NewNode().Init()
182 + apply(node)
183 + node.SetIPFSConfig("Provide.Enabled", true)
184 + // Configure a custom HTTP router for providing.
185 + // Using our mock server that will accept the provide requests.
186 + routingConf := map[string]any{
187 + "Type": "custom", // https://github.com/ipfs/kubo/blob/master/docs/delegated-routing.md#configuration-file-example
188 + "Methods": map[string]any{
189 + "provide": map[string]any{"RouterName": "MyCustomRouter"},
190 + "get-ipns": map[string]any{"RouterName": "MyCustomRouter"},
191 + "put-ipns": map[string]any{"RouterName": "MyCustomRouter"},
192 + "find-peers": map[string]any{"RouterName": "MyCustomRouter"},
193 + "find-providers": map[string]any{"RouterName": "MyCustomRouter"},
194 + },
195 + "Routers": map[string]any{
196 + "MyCustomRouter": map[string]any{
197 + "Type": "http",
198 + "Parameters": map[string]any{
199 + // Use the mock server URL
200 + "Endpoint": mockServer.URL,
201 + },
202 + },
203 + },
204 + }
205 + node.SetIPFSConfig("Routing", routingConf)
206 + node.StartDaemon()
207 + defer node.StopDaemon()
208 +
209 + cid := node.IPFSAddStr(time.Now().String())
210 + // The command should successfully provide via HTTP even without libp2p peers
211 + res := node.RunIPFS("routing", "provide", cid)
212 + assert.Empty(t, res.Stderr.String(), "Should have no errors when providing via HTTP router")
213 + assert.Equal(t, 0, res.ExitCode(), "Should succeed with exit code 0")
214 + })
215 +
216 // Right now Provide and Reprovide are tied together
217 t.Run("Reprovide.Interval=0 disables announcement of new CID too", func(t *testing.T) {
218 t.Parallel()