dotnet/aspnetcore · error

No usable HttpClient found.

Error message

No usable HttpClient found.

What it means

Thrown by the DefaultHttpClient constructor when neither a global fetch nor XMLHttpRequest is available in the runtime. DefaultHttpClient picks FetchHttpClient if fetch exists or it's Node, else XhrHttpClient if XMLHttpRequest exists, else throws this. It means the host environment is missing both HTTP primitives SignalR can use.

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 294cab2f9b)

Solutions

  1. On Node <18: install node-fetch and abort-controller and ensure they're required/polyfilled before constructing the connection.
  2. Upgrade to Node >=18 where global fetch is available.
  3. In non-browser runtimes, provide a custom HttpClient via HubConnectionBuilder.configureLogging/.withHttpClient (pass a custom HttpClient subclass) to inject fetch manually.
  4. In minimal JS engines, polyfill both fetch and XMLHttpRequest before starting the connection.

Example fix

// before (Node 16, no fetch)
const conn = new signalR.HubConnectionBuilder().withUrl('/hub').build();
// DefaultHttpClient throws 'No usable HttpClient found.'

// after
import fetch from 'node-fetch';
import AbortController from 'abort-controller';
(globalThis as any).fetch = fetch;
(globalThis as any).AbortController = AbortController;
const conn = new signalR.HubConnectionBuilder().withUrl('/hub').build();
Defensive patterns

Strategy: validation

Validate before calling

// detect the missing HTTP primitive BEFORE building the connection
function environmentSupportsHttp(): boolean {
  return typeof fetch !== 'undefined' || typeof XMLHttpRequest !== 'undefined';
}
if (!environmentSupportsHttp()) {
  throw new Error('This runtime lacks fetch and XMLHttpRequest; polyfill before starting SignalR.');
}

Type guard

function hasFetch(): boolean { return typeof fetch !== 'undefined'; }
function hasXhr(): boolean { return typeof XMLHttpRequest !== 'undefined'; }

Try / catch

try {
  const conn = new signalR.HubConnectionBuilder().withUrl('/hub').build();
} catch (e) {
  if (e.message === 'No usable HttpClient found.') {
    // polyfill fetch/XHR, then retry, or supply a custom HttpClient
    throw new Error('Install node-fetch/abort-controller or upgrade to Node >=18');
  }
  throw e;
}

Prevention

When it happens

Trigger: Constructing new DefaultHttpClient(logger) (done automatically by new HubConnectionBuilder()) in an environment with no fetch and no XHR: an old Node version (<18 with node-fetch not installed), a bare minimal JS engine, or a sandbox stripping globals.

Common situations: Running SignalR in an old Node (<18) without polyfilling fetch/node-fetch; a test runner or worker context lacking DOM APIs; a bundler misconfiguring the environment so Platform.isNode is false and globals are absent; React Native edge cases.

Related errors


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