Skip to content

Fix SSR issues for SignalR: require is not defined #19832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 28 additions & 27 deletions src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,36 @@ import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
import { Platform } from "./Utils";

let abortControllerType: { prototype: AbortController, new(): AbortController };
let fetchType: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
let jar: tough.CookieJar;
if (typeof fetch === "undefined") {
// In order to ignore the dynamic require in webpack builds we need to do this magic
// @ts-ignore: TS doesn't know about these names
const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;

// Cookies aren't automatically handled in Node so we need to add a CookieJar to preserve cookies across requests
jar = new (requireFunc("tough-cookie")).CookieJar();
fetchType = requireFunc("node-fetch");

// node-fetch doesn't have a nice API for getting and setting cookies
// fetch-cookie will wrap a fetch implementation with a default CookieJar or a provided one
fetchType = requireFunc("fetch-cookie")(fetchType, jar);

// Node needs EventListener methods on AbortController which our custom polyfill doesn't provide
abortControllerType = requireFunc("abort-controller");
} else {
fetchType = fetch;
abortControllerType = AbortController;
}

export class FetchHttpClient extends HttpClient {
private readonly abortControllerType: { prototype: AbortController, new(): AbortController };
private readonly fetchType: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
private readonly jar?: tough.CookieJar;

private readonly logger: ILogger;

public constructor(logger: ILogger) {
super();
this.logger = logger;

if (typeof fetch === "undefined") {
// In order to ignore the dynamic require in webpack builds we need to do this magic
// @ts-ignore: TS doesn't know about these names
const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;

// Cookies aren't automatically handled in Node so we need to add a CookieJar to preserve cookies across requests
this.jar = new (requireFunc("tough-cookie")).CookieJar();
this.fetchType = requireFunc("node-fetch");

// node-fetch doesn't have a nice API for getting and setting cookies
// fetch-cookie will wrap a fetch implementation with a default CookieJar or a provided one
this.fetchType = requireFunc("fetch-cookie")(this.fetchType, this.jar);

// Node needs EventListener methods on AbortController which our custom polyfill doesn't provide
this.abortControllerType = requireFunc("abort-controller");
} else {
this.fetchType = fetch.bind(self);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why call bind?

Copy link
Contributor Author

@hez2010 hez2010 Mar 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without calling bind, it will cause error if you try to invoke fetch:

Network error: TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation

It seems to be a bug coming from fetch, where a binding is set incorrectly. Calling fetch.bind(self) can resolve the problem, which will bind default fetch to "global" scope.

this.abortControllerType = AbortController;
}
}

/** @inheritDoc */
Expand All @@ -54,7 +55,7 @@ export class FetchHttpClient extends HttpClient {
throw new Error("No url defined.");
}

const abortController = new abortControllerType();
const abortController = new this.abortControllerType();

let error: any;
// Hook our abortSignal into the abort controller
Expand All @@ -79,7 +80,7 @@ export class FetchHttpClient extends HttpClient {

let response: Response;
try {
response = await fetchType(request.url!, {
response = await this.fetchType(request.url!, {
body: request.content!,
cache: "no-cache",
credentials: request.withCredentials === true ? "include" : "same-origin",
Expand Down Expand Up @@ -127,9 +128,9 @@ export class FetchHttpClient extends HttpClient {

public getCookieString(url: string): string {
let cookies: string = "";
if (Platform.isNode) {
if (Platform.isNode && this.jar) {
// @ts-ignore: unused variable
jar.getCookies(url, (e, c) => cookies = c.join("; "));
this.jar.getCookies(url, (e, c) => cookies = c.join("; "));
}
return cookies;
}
Expand Down
31 changes: 16 additions & 15 deletions src/SignalR/clients/ts/signalr/src/HttpConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { WebSocketTransport } from "./WebSocketTransport";

/** @private */
const enum ConnectionState {
Connecting = "Connecting ",
Connecting = "Connecting",
Connected = "Connected",
Disconnected = "Disconnected",
Disconnecting = "Disconnecting",
Expand All @@ -39,16 +39,6 @@ export interface IAvailableTransport {

const MAX_REDIRECTS = 100;

let WebSocketModule: any = null;
let EventSourceModule: any = null;
if (Platform.isNode && typeof require !== "undefined") {
// In order to ignore the dynamic require in webpack builds we need to do this magic
// @ts-ignore: TS doesn't know about these names
const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
WebSocketModule = requireFunc("ws");
EventSourceModule = requireFunc("eventsource");
}

/** @private */
export class HttpConnection implements IConnection {
private connectionState: ConnectionState;
Expand Down Expand Up @@ -88,19 +78,30 @@ export class HttpConnection implements IConnection {
throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");
}

let webSocketModule: any = null;
let eventSourceModule: any = null;

if (Platform.isNode && typeof require !== "undefined") {
// In order to ignore the dynamic require in webpack builds we need to do this magic
// @ts-ignore: TS doesn't know about these names
const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
webSocketModule = requireFunc("ws");
eventSourceModule = requireFunc("eventsource");
}

if (!Platform.isNode && typeof WebSocket !== "undefined" && !options.WebSocket) {
options.WebSocket = WebSocket;
} else if (Platform.isNode && !options.WebSocket) {
if (WebSocketModule) {
options.WebSocket = WebSocketModule;
if (webSocketModule) {
options.WebSocket = webSocketModule;
}
}

if (!Platform.isNode && typeof EventSource !== "undefined" && !options.EventSource) {
options.EventSource = EventSource;
} else if (Platform.isNode && !options.EventSource) {
if (typeof EventSourceModule !== "undefined") {
options.EventSource = EventSourceModule;
if (typeof eventSourceModule !== "undefined") {
options.EventSource = eventSourceModule;
}
}

Expand Down