Skip to content

Commit 87b25d5

Browse files
committed
Adding a wrapper for fetch to handle error responses.
1 parent e5b7acb commit 87b25d5

File tree

6 files changed

+127
-7
lines changed

6 files changed

+127
-7
lines changed

src/misc/httpClient.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import { CsWebLibHttpResponseError, httpRequest } from "./httpClient";
3+
4+
describe("CsWebLibHttpResponseError", () => {
5+
it("should create an error with the correct name", () => {
6+
const error = new CsWebLibHttpResponseError("Test error", 404);
7+
expect(error.name).toBe("CsWebLibHttpError");
8+
});
9+
10+
it("should store the message passed to the constructor", () => {
11+
const errorMessage = "Test error message";
12+
const error = new CsWebLibHttpResponseError(errorMessage, 404);
13+
expect(error.message).toBe(errorMessage);
14+
});
15+
16+
it("should store the HTTP response code", () => {
17+
const responseCode = 404;
18+
const error = new CsWebLibHttpResponseError("Test error", responseCode);
19+
expect(error.responseCode).toBe(responseCode);
20+
});
21+
22+
it("should extend Error class", () => {
23+
const error = new CsWebLibHttpResponseError("Test error", 404);
24+
expect(error).toBeInstanceOf(Error);
25+
});
26+
});
27+
28+
describe("httpRequest", () => {
29+
const mockFetch = vi.fn();
30+
global.fetch = mockFetch;
31+
32+
beforeEach(() => {
33+
mockFetch.mockReset();
34+
});
35+
36+
afterEach(() => {
37+
vi.resetAllMocks();
38+
});
39+
40+
it("should call fetch on the URL", async () => {
41+
const testUrl = "https://diamond.ac.uk/api";
42+
const mockResponse = {
43+
status: 200,
44+
statusText: "OK"
45+
};
46+
47+
mockFetch.mockResolvedValueOnce(mockResponse);
48+
49+
const result = await httpRequest(testUrl);
50+
51+
expect(mockFetch).toHaveBeenCalledWith(testUrl);
52+
expect(result).toBe(mockResponse);
53+
});
54+
55+
it("should call fetch with the URL and init argument", async () => {
56+
const testUrl = "https://diamond.ac.uk/api";
57+
const initOptions = {
58+
method: "POST",
59+
headers: { "Content-Type": "application/json" }
60+
};
61+
const mockResponse = {
62+
status: 201,
63+
statusText: "Created"
64+
};
65+
66+
mockFetch.mockResolvedValueOnce(mockResponse);
67+
68+
const result = await httpRequest(testUrl, initOptions);
69+
70+
expect(mockFetch).toHaveBeenCalledWith(testUrl, initOptions);
71+
expect(result).toBe(mockResponse);
72+
});
73+
74+
it("should throw CsWebLibHttpResponseError for 404 response", async () => {
75+
const testUrl = "https://diamond.ac.uk/api";
76+
const mockResponse = {
77+
status: 404,
78+
statusText: "Not Found"
79+
};
80+
81+
mockFetch.mockResolvedValueOnce(mockResponse);
82+
83+
await expect(httpRequest(testUrl)).rejects.toThrow(
84+
CsWebLibHttpResponseError
85+
);
86+
});
87+
});

src/misc/httpClient.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export class CsWebLibHttpResponseError extends Error {
2+
private _responseCode: number;
3+
4+
constructor(message: string, httpResponseCode: number) {
5+
super(message);
6+
this.name = "CsWebLibHttpError";
7+
this._responseCode = httpResponseCode;
8+
}
9+
10+
get responseCode(): number {
11+
return this._responseCode;
12+
}
13+
}
14+
15+
export const httpRequest = async (
16+
url: string,
17+
init?: RequestInit
18+
): Promise<Response> => {
19+
const response = init ? await fetch(url, init) : await fetch(url);
20+
21+
if (response?.status >= 400) {
22+
const message = `HTTP GET failed for url: ${url}.\nResponse code ${response?.status}\nResponse message ${response?.statusText}\n `;
23+
throw new CsWebLibHttpResponseError(message, response.status);
24+
}
25+
26+
return response;
27+
};

src/misc/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { onRenderCallback } from "./profilerCallback";
22
export { FileProvider, FileContext } from "./fileContext";
33
export { OutlineContext, OutlineProvider } from "./outlineContext";
4+
export { CsWebLibHttpResponseError, httpRequest } from "./httpClient";

src/ui/hooks/useArchivedData.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TimeSeriesPoint } from "../widgets/StripChart/stripChart";
44
import { convertStringTimePeriod, trimArchiveData } from "../widgets/utils";
55
import { useState, useEffect } from "react";
66
import { Plt } from "../../types/plt";
7+
import { httpRequest } from "../../misc/httpClient";
78

89
/**
910
* Fetch archived data for each PV from archivers available
@@ -40,7 +41,7 @@ export function useArchivedData(plt: Plt): [TimeSeriesPoint[], boolean] {
4041
for (const url of Object.values(archivers)) {
4142
let tmpData: any[] = fetchedData;
4243
try {
43-
const resp = await fetch(`${url}${timeString}`);
44+
const resp = await httpRequest(`${url}${timeString}`);
4445
const json = await resp.json();
4546
json.forEach((data: any) => {
4647
// Trim each dataset down and push into fetchedData

src/ui/widgets/EmbeddedDisplay/pltParser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { parseChildProps, ParserDict } from "./parser";
1111
import { Axis } from "../../../types/axis";
1212
import { Archiver, Trace } from "../../../types/trace";
1313
import { Plt } from "../../../types/plt";
14+
import { httpRequest } from "../../../misc/httpClient";
1415

1516
const PLT_PARSERS: ParserDict = {
1617
start: ["start", opiParseString],
@@ -220,7 +221,7 @@ async function fetchPltFile(file: string, parentDir: string) {
220221
if (parentDir && !file.startsWith("http")) {
221222
file = normalisePath(file, parentDir);
222223
}
223-
const filePromise = await fetch(file);
224+
const filePromise = await httpRequest(file);
224225
const contents = await filePromise.text();
225226
// Convert it to a "compact format"
226227
const compactJSON = xml2js(contents, {

src/ui/widgets/EmbeddedDisplay/useOpiFile.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { errorWidget, WidgetDescription } from "../createComponent";
66
import { parseBob } from "./bobParser";
77
import { parseJson } from "./jsonParser";
88
import { parseOpi } from "./opiParser";
9+
import { httpRequest } from "../../../misc/httpClient";
910

1011
// Global cache. Should perhaps be put in a context.
1112
const fileCache: FileCache = {};
@@ -32,12 +33,14 @@ async function fetchAndConvert(
3233
filepath: string,
3334
protocol: string
3435
): Promise<WidgetDescription> {
35-
const parentDir = filepath.substr(0, filepath.lastIndexOf("/"));
36-
const filePromise = await fetch(filepath);
37-
const fileExt = filepath.split(".").pop() || "json";
38-
const contents = await filePromise.text();
39-
let description = EMPTY_WIDGET;
4036
try {
37+
const parentDir = filepath.substr(0, filepath.lastIndexOf("/"));
38+
const fileResponse = await httpRequest(filepath);
39+
40+
const fileExt = filepath.split(".").pop() || "json";
41+
const contents = await fileResponse.text();
42+
let description = EMPTY_WIDGET;
43+
4144
// Hack!
4245
if (contents.startsWith("<!DOCTYPE html>")) {
4346
throw new Error("File not found");

0 commit comments

Comments
 (0)