Skip to content

Commit

Permalink
Make responseType mandatory
Browse files Browse the repository at this point in the history
  • Loading branch information
zoontek committed Apr 17, 2024
1 parent 7c69610 commit 4dd9492
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 39 deletions.
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,49 @@ $ npm install --save @swan-io/request @swan-io/boxed
import { Request, badStatusToError, emptyToError } from "@swan-io/request";

// Regular case
Request.make({ url: "/api/health" }).onResolve(console.log);
Request.make({
url: "/api/health",
responseType: "text",
}).onResolve(console.log);
// Result.Ok({status: 200, ok: true, response: Option.Some("{\"ok\":true}")})

// Timeout
Request.make({ url: "/api/health", timeout: 2000 }).onResolve(console.log);
Request.make({
url: "/api/health",
responseType: "text",
timeout: 2000,
}).onResolve(console.log);
// Result.Error(TimeoutError)

// Network error
Request.make({ url: "/api/health" }).onResolve(console.log);
Request.make({
url: "/api/health",
responseType: "text",
}).onResolve(console.log);
// Result.Error(NetworkError)

// Custom response type
Request.make({ url: "/api/health", responseType: "json" }).onResolve(
console.log,
);
Request.make({
url: "/api/health",
responseType: "json",
}).onResolve(console.log);
// Result.Ok({status: 200, ok: true, response: Option.Some({ok: true})})

// Handle empty response as an error
Request.make({ url: "/api/health" })
Request.make({ url: "/api/health", responseType: "json" })
.mapOkToResult(emptyToError)
.onResolve(console.log);
// Result.Error(EmptyResponseError)

// Handle bad status as an error
Request.make({ url: "/api/health" })
Request.make({ url: "/api/health", responseType: "text" })
.mapOkToResult(badStatusToError)
.onResolve(console.log);
// Result.Error(BadStatusError)

// Cancel request
useEffect(() => {
const future = Request.make({ url: "/api/health" });
const future = Request.make({ url: "/api/health", responseType: "text" });
return () => future.cancel();
}, []);
```
Expand Down
61 changes: 34 additions & 27 deletions src/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ type Method = "GET" | "POST" | "OPTIONS" | "PATCH" | "PUT" | "DELETE";

export class NetworkError extends Error {
url: string;

constructor(url: string) {
super(`Request to ${url} failed`);
Object.setPrototypeOf(this, NetworkError.prototype);

this.name = "NetworkError";
this.url = url;
}
Expand All @@ -34,13 +36,11 @@ export class NetworkError extends Error {
export class TimeoutError extends Error {
url: string;
timeout: number | undefined;

constructor(url: string, timeout?: number) {
if (timeout == undefined) {
super(`Request to ${url} timed out`);
} else {
super(`Request to ${url} timed out (> ${timeout}ms)`);
}
super(`Request to ${url} timed out` + (timeout ? ` (> ${timeout}ms)` : ""));
Object.setPrototypeOf(this, TimeoutError.prototype);

this.name = "TimeoutError";
this.url = url;
this.timeout = timeout;
Expand All @@ -49,34 +49,34 @@ export class TimeoutError extends Error {

type Config<T extends ResponseType> = {
url: string;
method?: Method;
responseType?: T;
body?: Document | XMLHttpRequestBodyInit;
headers?: Record<string, string>;
body?: Document | XMLHttpRequestBodyInit;
method?: Method;
responseType: T;
timeout?: number;
withCredentials?: boolean;
onLoadStart?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
onProgress?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
timeout?: number;
onLoadStart?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
};

export type Response<T> = {
url: string;
status: number;
ok: boolean;
response: Option<T>;
status: number;
url: string;
xhr: XMLHttpRequest;
};

const make = <T extends ResponseType = "text">({
const make = <T extends ResponseType>({
url,
headers,
body,
method = "GET",
responseType,
body,
headers,
timeout,
withCredentials = false,
onLoadStart,
onProgress,
timeout,
onLoadStart,
}: Config<T>): Future<
Result<Response<ResponseTypeMap[T]>, NetworkError | TimeoutError>
> => {
Expand All @@ -85,18 +85,20 @@ const make = <T extends ResponseType = "text">({
>((resolve) => {
const xhr = new XMLHttpRequest();
xhr.withCredentials = withCredentials;

// Only allow asynchronous requests
xhr.open(method, url, true);

// If `responseType` is unspecified, XHR defaults to `text`
if (responseType != undefined) {
if (responseType != null) {
xhr.responseType = responseType;
}

if (timeout != undefined) {
if (timeout != null) {
xhr.timeout = timeout;
}

if (headers != undefined) {
if (headers != null) {
Dict.entries(headers).forEach(([key, value]) =>
xhr.setRequestHeader(key, value),
);
Expand All @@ -114,6 +116,7 @@ const make = <T extends ResponseType = "text">({

const onLoad = () => {
cleanupEvents();

const status = xhr.status;
// Response can be empty, which is why we represent it as an option.
// We provide the `emptyToError` helper to handle this case.
Expand All @@ -135,21 +138,23 @@ const make = <T extends ResponseType = "text">({
xhr.removeEventListener("error", onError);
xhr.removeEventListener("load", onLoad);
xhr.removeEventListener("timeout", onTimeout);
if (onLoadStart != undefined) {

if (onLoadStart != null) {
xhr.removeEventListener("loadstart", onLoadStart);
}
if (onProgress != undefined) {
if (onProgress != null) {
xhr.removeEventListener("progress", onProgress);
}
};

xhr.addEventListener("error", onError);
xhr.addEventListener("load", onLoad);
xhr.addEventListener("timeout", onTimeout);
if (onLoadStart != undefined) {

if (onLoadStart != null) {
xhr.addEventListener("loadstart", onLoadStart);
}
if (onProgress != undefined) {
if (onProgress != null) {
xhr.addEventListener("progress", onProgress);
}

Expand All @@ -167,9 +172,11 @@ export class BadStatusError extends Error {
url: string;
status: number;
response: unknown;

constructor(url: string, status: number, response?: unknown) {
super(`Request to ${url} gave status ${status}`);
Object.setPrototypeOf(this, BadStatusError.prototype);

this.name = "BadStatusError";
this.url = url;
this.status = status;
Expand All @@ -193,9 +200,11 @@ export const badStatusToError = <T>(

export class EmptyResponseError extends Error {
url: string;

constructor(url: string) {
super(`Request to ${url} gave an empty response`);
Object.setPrototypeOf(this, EmptyResponseError.prototype);

this.name = "EmptyResponseError";
this.url = url;
}
Expand All @@ -205,6 +214,4 @@ export const emptyToError = <T>(response: Response<T>) => {
return response.response.toResult(new EmptyResponseError(response.url));
};

export const Request = {
make,
};
export const Request = { make };
15 changes: 12 additions & 3 deletions test/Request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { Request, emptyToError } from "../src/Request";
import { Option, Result } from "@swan-io/boxed";

test("Request: basic", async () => {
return Request.make({ url: "data:text/plain,hello!" }).tap((value) => {
return Request.make({
url: "data:text/plain,hello!",
responseType: "text",
}).tap((value) => {
expect(value.map((value) => value.status)).toEqual(Result.Ok(200));
expect(value.map((value) => value.response)).toEqual(
Result.Ok(Option.Some("hello!")),
Expand All @@ -13,15 +16,21 @@ test("Request: basic", async () => {
});

test("Request: emptyToError", async () => {
return Request.make({ url: "data:text/plain,hello!" })
return Request.make({
url: "data:text/plain,hello!",
responseType: "text",
})
.mapOkToResult(emptyToError)
.tap((value) => {
expect(value).toEqual(Result.Ok("hello!"));
});
});

test("Request: JSON as text", async () => {
return Request.make({ url: 'data:text/json,{"ok":true}' }).tap((value) => {
return Request.make({
url: 'data:text/json,{"ok":true}',
responseType: "text",
}).tap((value) => {
expect(value.map((value) => value.status)).toEqual(Result.Ok(200));
expect(value.map((value) => value.response)).toEqual(
Result.Ok(Option.Some('{"ok":true}')),
Expand Down

0 comments on commit 4dd9492

Please sign in to comment.