dotnet/aspnetcore · critical · Error

No usable HttpClient found.

Error message

No usable HttpClient found.

What it means

DefaultHttpClient picks an HTTP backend at construction time: it prefers the Fetch API (or Node's fetch/Platform.isNode), then falls back to XMLHttpRequest. If neither global is present, there is no transport available to perform HTTP requests, so the constructor throws synchronously.

Solutions

  1. Upgrade to Node.js 18+ where fetch is a global, or install and register a fetch polyfill (e.g. node-fetch assigned to globalThis.fetch) before constructing the client.
  2. If you must support an older environment, supply your own HttpClient implementation to HttpConnection via options.httpClient.
  3. Verify that the bundler is not stripping window.fetch/XMLHttpRequest via dead-code elimination; mark the SignalR package as external or add the globals to your platform.
  4. In React Native, ensure the networking globals are loaded before any SignalR import.

Example fix

// before: Node 16, no fetch global
const connection = new signalR.HubConnectionBuilder().withUrl(url).build();

// after: polyfill fetch before importing signalr
import { fetch as undiciFetch } from "undici";
(globalThis as any).fetch = (globalThis as any).fetch ?? undiciFetch;
import * as signalR from "@microsoft/signalr";
const connection = new signalR.HubConnectionBuilder().withUrl(url).build();
Defensive patterns

Strategy: validation

Validate before calling

function hasHttpBackend(): boolean {
  return typeof fetch !== "undefined" || typeof XMLHttpRequest !== "undefined";
}
if (!hasHttpBackend()) {
  // register polyfills before importing signalr
  throw new Error("Environment has no HTTP backend — install node-fetch or upgrade Node.");
}

Type guard

function supportsDefaultHttpClient(): boolean {
  return typeof fetch !== "undefined" || typeof XMLHttpRequest !== "undefined" || (typeof process !== "undefined" && process.versions?.node != null && typeof globalThis.fetch === "function");
}

Try / catch

try {
  const client = new signalR.HttpClient(...);
} catch (e) {
  if (e instanceof Error && e.message.includes("No usable HttpClient")) {
    // install polyfill and retry, or supply a custom HttpClient
  }
  throw e;
}

Prevention

When it happens

Trigger: Instantiating new DefaultHttpClient(logger) in a JavaScript runtime that exposes neither global fetch nor global XMLHttpRequest. Common in older Node.js (< 18, no built-in fetch) without a polyfill, or in a minimal/embedded JS engine (QuickJS, duktape, some React Native renderers without the network polyfill).

Common situations: Running the SignalR client on Node.js 16 or earlier (fetch is not global there). Bundling for a non-browser target with tree-shaking that strips the fetch polyfill. Using the client inside a Web Worker context that lacks XMLHttpRequest in some browsers, when fetch detection fails.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/e93e6b01da6595d1. Report an issue: GitHub.

Appendix: source

Thrown at src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts:24

import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger } from "./ILogger";
import { Platform } from "./Utils";
import { XhrHttpClient } from "./XhrHttpClient";

/** Default implementation of {@link @microsoft/signalr.HttpClient}. */
export class DefaultHttpClient extends HttpClient {
    private readonly _httpClient: HttpClient;

    /** Creates a new instance of the {@link @microsoft/signalr.DefaultHttpClient}, using the provided {@link @microsoft/signalr.ILogger} to log messages. */
    public constructor(logger: ILogger) {
        super();

        if (typeof fetch !== "undefined" || Platform.isNode) {
            this._httpClient = new FetchHttpClient(logger);
        } else if (typeof XMLHttpRequest !== "undefined") {
            this._httpClient = new XhrHttpClient(logger);
        } else {
            throw new Error("No usable HttpClient found.");
        }
    }

    /** @inheritDoc */
    public send(request: HttpRequest): Promise<HttpResponse> {
        // Check that abort was not signaled before calling send
        if (request.abortSignal && request.abortSignal.aborted) {
            return Promise.reject(new AbortError());
        }

        if (!request.method) {
            return Promise.reject(new Error("No method defined."));
        }
        if (!request.url) {
            return Promise.reject(new Error("No url defined."));
        }

        return this._httpClient.send(request);

View on GitHub (pinned to 3600ca084e)