@cryptotaxi247 / kubo / commits / 23ba660ef

fix(routing): use LegacyProvider for HTTP-only custom routing (#11112)

* fix(routing): use LegacyProvider for HTTP-only custom routing when `Routing.Type=custom` with only HTTP routers and no DHT, fall back to LegacyProvider instead of SweepingProvider. SweepingProvider requires a DHT client which is unavailable in HTTP-only configurations, causing it to return NoopProvider and breaking provider record announcements to HTTP routers. fixes #11089 * test(routing): verify provide stat works with HTTP-only routing * docs(config): clarify SweepEnabled fallback for HTTP-only routing --------- Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>

Marcin Rataj committed Jan 9, 2026 at 18:23 UTC 23ba660ef07a282e5fc3f4b0ad44dae107c74f3f
3 files changed +133 -1
core/node/provider.go
+51 -1
@@ -692,6 +692,48 @@ See docs: https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtmaxw
692
693 // ONLINE/OFFLINE
694
695 +// hasDHTRouting checks if the routing configuration includes a DHT component.
696 +// Returns false for HTTP-only custom routing configurations (e.g., Routing.Type="custom"
697 +// with only HTTP routers). This is used to determine whether SweepingProviderOpt
698 +// can be used, since it requires a DHT client.
699 +func hasDHTRouting(cfg *config.Config) bool {
700 + routingType := cfg.Routing.Type.WithDefault(config.DefaultRoutingType)
701 + switch routingType {
702 + case "auto", "autoclient", "dht", "dhtclient", "dhtserver":
703 + return true
704 + case "custom":
705 + // Check if any router in custom config is DHT-based
706 + for _, router := range cfg.Routing.Routers {
707 + if routerIncludesDHT(router, cfg) {
708 + return true
709 + }
710 + }
711 + return false
712 + default: // "none", "delegated"
713 + return false
714 + }
715 +}
716 +
717 +// routerIncludesDHT recursively checks if a router configuration includes DHT.
718 +// Handles parallel and sequential composite routers by checking their children.
719 +func routerIncludesDHT(rp config.RouterParser, cfg *config.Config) bool {
720 + switch rp.Type {
721 + case config.RouterTypeDHT:
722 + return true
723 + case config.RouterTypeParallel, config.RouterTypeSequential:
724 + if children, ok := rp.Parameters.(*config.ComposableRouterParams); ok {
725 + for _, child := range children.Routers {
726 + if childRouter, exists := cfg.Routing.Routers[child.RouterName]; exists {
727 + if routerIncludesDHT(childRouter, cfg) {
728 + return true
729 + }
730 + }
731 + }
732 + }
733 + }
734 + return false
735 +}
736 +
737 // OnlineProviders groups units managing provide routing records online
738 func OnlineProviders(provide bool, cfg *config.Config) fx.Option {
739 if !provide {
@@ -708,7 +750,15 @@ func OnlineProviders(provide bool, cfg *config.Config) fx.Option {
750 opts := []fx.Option{
751 fx.Provide(setReproviderKeyProvider(providerStrategy)),
752 }
711 - if cfg.Provide.DHT.SweepEnabled.WithDefault(config.DefaultProvideDHTSweepEnabled) {
753 +
754 + sweepEnabled := cfg.Provide.DHT.SweepEnabled.WithDefault(config.DefaultProvideDHTSweepEnabled)
755 + dhtAvailable := hasDHTRouting(cfg)
756 +
757 + // Use SweepingProvider only when both sweep is enabled AND DHT is available.
758 + // For HTTP-only routing (e.g., Routing.Type="custom" with only HTTP routers),
759 + // fall back to LegacyProvider which works with ProvideManyRouter.
760 + // See https://github.com/ipfs/kubo/issues/11089
761 + if sweepEnabled && dhtAvailable {
762 opts = append(opts, SweepingProviderOpt(cfg))
763 } else {
764 reprovideInterval := cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval)
docs/config.md
+3
@@ -2195,6 +2195,9 @@ You can compare the effectiveness of sweep mode vs legacy mode by monitoring the
2195 > [!NOTE]
2196 > This is the default provider system as of Kubo v0.39. To use the legacy provider instead, set `Provide.DHT.SweepEnabled=false`.
2197
2198 +> [!NOTE]
2199 +> When DHT routing is unavailable (e.g., `Routing.Type=custom` with only HTTP routers), the provider automatically falls back to the legacy provider regardless of this setting.
2200 +
2201 Default: `true`
2202
2203 Type: `flag`
test/cli/provider_test.go
+79
@@ -7,6 +7,7 @@ import (
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 + "sync/atomic"
11 "testing"
12 "time"
13
@@ -764,3 +765,81 @@ func TestProvider(t *testing.T) {
765 })
766 }
767 }
768 +
769 +// TestHTTPOnlyProviderWithSweepEnabled tests that provider records are correctly
770 +// sent to HTTP routers when Routing.Type="custom" with only HTTP routers configured,
771 +// even when Provide.DHT.SweepEnabled=true (the default since v0.39).
772 +//
773 +// This is a regression test for https://github.com/ipfs/kubo/issues/11089
774 +func TestHTTPOnlyProviderWithSweepEnabled(t *testing.T) {
775 + t.Parallel()
776 +
777 + // Track provide requests received by the mock HTTP router
778 + var provideRequests atomic.Int32
779 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
780 + if (r.Method == http.MethodPut || r.Method == http.MethodPost) &&
781 + strings.HasPrefix(r.URL.Path, "/routing/v1/providers") {
782 + provideRequests.Add(1)
783 + w.WriteHeader(http.StatusOK)
784 + } else if strings.HasPrefix(r.URL.Path, "/routing/v1/providers") && r.Method == http.MethodGet {
785 + // Return empty providers for findprovs
786 + w.Header().Set("Content-Type", "application/x-ndjson")
787 + w.WriteHeader(http.StatusOK)
788 + } else {
789 + w.WriteHeader(http.StatusNotFound)
790 + }
791 + }))
792 + defer mockServer.Close()
793 +
794 + h := harness.NewT(t)
795 + node := h.NewNode().Init()
796 +
797 + // Explicitly set SweepEnabled=true (the default since v0.39, but be explicit for test clarity)
798 + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true)
799 + node.SetIPFSConfig("Provide.Enabled", true)
800 +
801 + // Configure HTTP-only custom routing (no DHT) with explicit Routing.Type=custom
802 + routingConf := map[string]any{
803 + "Type": "custom", // Explicitly set Routing.Type=custom
804 + "Methods": map[string]any{
805 + "provide": map[string]any{"RouterName": "HTTPRouter"},
806 + "get-ipns": map[string]any{"RouterName": "HTTPRouter"},
807 + "put-ipns": map[string]any{"RouterName": "HTTPRouter"},
808 + "find-peers": map[string]any{"RouterName": "HTTPRouter"},
809 + "find-providers": map[string]any{"RouterName": "HTTPRouter"},
810 + },
811 + "Routers": map[string]any{
812 + "HTTPRouter": map[string]any{
813 + "Type": "http",
814 + "Parameters": map[string]any{
815 + "Endpoint": mockServer.URL,
816 + },
817 + },
818 + },
819 + }
820 + node.SetIPFSConfig("Routing", routingConf)
821 + node.StartDaemon()
822 + defer node.StopDaemon()
823 +
824 + // Add content and manually provide it
825 + cid := node.IPFSAddStr(time.Now().String())
826 +
827 + // Manual provide should succeed even without libp2p peers
828 + res := node.RunIPFS("routing", "provide", cid)
829 + // Check that the command succeeded (exit code 0) and no provide-related errors
830 + assert.Equal(t, 0, res.ExitCode(), "routing provide should succeed with HTTP-only routing and SweepEnabled=true")
831 + assert.NotContains(t, res.Stderr.String(), "cannot provide", "should not have provide errors")
832 +
833 + // Verify HTTP router received at least one provide request
834 + assert.Greater(t, provideRequests.Load(), int32(0),
835 + "HTTP router should have received provide requests")
836 +
837 + // Verify 'provide stat' works with HTTP-only routing (regression test for stats)
838 + statRes := node.RunIPFS("provide", "stat")
839 + assert.Equal(t, 0, statRes.ExitCode(), "provide stat should succeed with HTTP-only routing")
840 + assert.NotContains(t, statRes.Stderr.String(), "stats not available",
841 + "should not report stats unavailable")
842 + // LegacyProvider outputs "TotalReprovides:" in its stats
843 + assert.Contains(t, statRes.Stdout.String(), "TotalReprovides:",
844 + "should show legacy provider stats")
845 +}