Skip to content

Commit

Permalink
Validate URLs when constructing Sarus object
Browse files Browse the repository at this point in the history
This will better inform a user about invalid URLs when constructing a
new Sarus object. It will already give an error during construction
instead of the connect step. That way, we won't repeatedly try to
connect to the same URL, despite it being invalid the whole time.

Add test case in __tests__ for

- Completely invalid URL (fails to be fed into new URL())
- Invalid protocol (must be ws / wss)

We use stock JS functionality for this, so no custom URL parser.
  • Loading branch information
justuswilhelm committed Oct 16, 2023
1 parent 2f84d15 commit 15234b9
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 2 deletions.
20 changes: 20 additions & 0 deletions __tests__/index/connectionOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ describe("connection options", () => {
server.close();
});

it("should correctly validate invalid WebSocket URLs", () => {
// Testing with jest-websocket-mock will not give us a TypeError here.
// We re-throw the error therefore. Testing it in a browser we can
// see that a TypeError is handled correctly.
expect(() => {
new Sarus({ url: "invalid-url" });
}).toThrow("invalid");

expect(() => {
new Sarus({ url: "http://wrong-protocol" });
}).toThrow("have protocol");

expect(() => {
new Sarus({ url: "https://also-wrong-protocol" });
}).toThrow("have protocol");

new Sarus({ url: "ws://this-will-pass" });
new Sarus({ url: "wss://this-too-shall-pass" });
});

it("should set the WebSocket protocols value to an empty string if nothing is passed", async () => {
const sarus: Sarus = new Sarus({ url });
await server.connected;
Expand Down
31 changes: 29 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// File Dependencies
import {
ALLOWED_PROTOCOLS,
WS_EVENT_NAMES,
DATA_STORAGE_TYPES,
DEFAULT_EVENT_LISTENERS_OBJECT,
Expand Down Expand Up @@ -46,6 +47,32 @@ const getMessagesFromStore = ({ storageType, storageKey }: StorageParams) => {
}
};

const validateWebSocketUrl = (rawUrl: string): URL => {
let url: URL;
try {
// Alternatively, we can also check with URL.canParse(), but since we need
// the URL object anyway to validate the protocol, we go ahead and parse it
// here.
url = new URL(rawUrl);
} catch (e) {
// TypeError, as specified by WHATWG URL Standard:
// https://url.spec.whatwg.org/#url-class (see constructor steps)
if (!(e instanceof TypeError)) {
throw e;
}
// Untested - our URL mock does not give us an instance of TypeError
const { message } = e;
throw new Error(`The WebSocket URL is not valid: ${message}`);
}
const { protocol } = url;
if (!ALLOWED_PROTOCOLS.includes(protocol)) {
throw new Error(
`Expected the WebSocket URL to have protocol 'ws:' or 'wss:', got '${protocol}' instead.`,
);
}
return url;
};

export interface SarusClassParams {
url: string;
binaryType?: BinaryType;
Expand Down Expand Up @@ -75,7 +102,7 @@ export interface SarusClassParams {
*/
export default class Sarus {
// Constructor params
url: string;
url: URL;
binaryType?: BinaryType;
protocols?: string | Array<string>;
eventListeners: EventListenersInterface;
Expand Down Expand Up @@ -106,7 +133,7 @@ export default class Sarus {
this.eventListeners = this.auditEventListeners(eventListeners);

// Sets the WebSocket server url for the client to connect to.
this.url = url;
this.url = validateWebSocketUrl(url);

// Sets the binaryType of the data being sent over the connection
this.binaryType = binaryType;
Expand Down
2 changes: 2 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Dependencies
import { EventListenersInterface } from "./validators";

export const ALLOWED_PROTOCOLS: Array<string> = ["ws:", "wss:"];

/**
* A definitive list of events for a WebSocket client to listen on
* @constant
Expand Down

0 comments on commit 15234b9

Please sign in to comment.