This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/app/utils/ApiClient.js

199 lines
5.1 KiB
JavaScript
Raw Normal View History

2017-05-12 00:23:56 +00:00
// @flow
import retry from "fetch-retry";
import invariant from "invariant";
import { map, trim } from "lodash";
import { getCookie } from "tiny-cookie";
import stores from "stores";
import download from "./download";
import {
AuthorizationError,
2020-12-22 05:10:25 +00:00
BadRequestError,
NetworkError,
NotFoundError,
OfflineError,
RequestError,
2020-12-22 05:10:25 +00:00
ServiceUnavailableError,
UpdateRequiredError,
} from "./errors";
2016-02-27 21:53:11 +00:00
2017-05-12 00:23:56 +00:00
type Options = {
baseUrl?: string,
};
// authorization cookie set by a Cloudflare Access proxy
const CF_AUTHORIZATION = getCookie("CF_Authorization");
// if the cookie is set, we must pass it with all ApiClient requests
const CREDENTIALS = CF_AUTHORIZATION ? "same-origin" : "omit";
const fetchWithRetry = retry(fetch);
2016-02-27 21:53:11 +00:00
class ApiClient {
2017-05-12 00:23:56 +00:00
baseUrl: string;
userAgent: string;
constructor(options: Options = {}) {
this.baseUrl = options.baseUrl || "/api";
this.userAgent = "OutlineFrontend";
2016-02-27 21:53:11 +00:00
}
2018-06-08 04:35:40 +00:00
fetch = async (
2017-05-12 00:23:56 +00:00
path: string,
method: string,
data: ?Object | FormData | void,
2017-05-12 00:23:56 +00:00
options: Object = {}
) => {
2016-02-27 21:53:11 +00:00
let body;
let modifiedPath;
let urlToFetch;
let isJson;
2016-02-27 21:53:11 +00:00
if (method === "GET") {
2017-05-12 00:23:56 +00:00
if (data) {
modifiedPath = `${path}?${data && this.constructQueryString(data)}`;
} else {
modifiedPath = path;
}
} else if (method === "POST" || method === "PUT") {
body = data || undefined;
// Only stringify data if its a normal object and
// not if it's [object FormData], in addition to
// toggling Content-Type to application/json
if (
typeof data === "object" &&
(data || "").toString() === "[object Object]"
) {
isJson = true;
body = JSON.stringify(data);
}
2016-02-27 21:53:11 +00:00
}
if (path.match(/^http/)) {
urlToFetch = modifiedPath || path;
} else {
urlToFetch = this.baseUrl + (modifiedPath || path);
}
let headerOptions: any = {
Accept: "application/json",
"cache-control": "no-cache",
"x-editor-version": EDITOR_VERSION,
pragma: "no-cache",
};
// for multipart forms or other non JSON requests fetch
// populates the Content-Type without needing to explicitly
// set it.
if (isJson) {
headerOptions["Content-Type"] = "application/json";
}
const headers = new Headers(headerOptions);
2017-05-30 02:08:03 +00:00
if (stores.auth.authenticated) {
invariant(stores.auth.token, "JWT token not set properly");
headers.set("Authorization", `Bearer ${stores.auth.token}`);
2016-02-27 21:53:11 +00:00
}
let response;
try {
response = await fetchWithRetry(urlToFetch, {
method,
body,
headers,
redirect: "follow",
credentials: CREDENTIALS,
cache: "no-cache",
});
} catch (err) {
if (window.navigator.onLine) {
throw new NetworkError("A network error occurred, try again?");
} else {
throw new OfflineError("No internet connection available");
}
}
2016-02-27 21:53:11 +00:00
const success = response.status >= 200 && response.status < 300;
if (options.download && success) {
const blob = await response.blob();
const fileName = (
response.headers.get("content-disposition") || ""
).split("filename=")[1];
download(blob, trim(fileName, '"'));
return;
} else if (success && response.status === 204) {
return;
} else if (success) {
2018-06-08 04:35:40 +00:00
return response.json();
}
2017-04-27 04:47:03 +00:00
2018-06-08 04:35:40 +00:00
// Handle 401, log out user
if (response.status === 401) {
stores.auth.logout();
return;
}
2017-04-27 04:47:03 +00:00
2018-06-08 04:35:40 +00:00
// Handle failed responses
const error = {};
error.statusCode = response.status;
error.response = response;
2018-11-04 04:47:46 +00:00
try {
const parsed = await response.json();
error.message = parsed.message || "";
error.error = parsed.error;
error.data = parsed.data;
2018-11-04 04:47:46 +00:00
} catch (_err) {
// we're trying to parse an error so JSON may not be valid
}
if (response.status === 400 && error.error === "editor_update_required") {
window.location.reload(true);
throw new UpdateRequiredError(error.message);
}
2020-12-22 05:10:25 +00:00
if (response.status === 400) {
throw new BadRequestError(error.message);
}
if (response.status === 403) {
if (error.error === "user_suspended") {
stores.auth.logout();
return;
}
throw new AuthorizationError(error.message);
}
if (response.status === 404) {
throw new NotFoundError(error.message);
}
2020-12-22 05:10:25 +00:00
if (response.status === 503) {
throw new ServiceUnavailableError(error.message);
}
throw new RequestError(error.message);
2017-04-27 04:47:03 +00:00
};
2016-02-27 21:53:11 +00:00
2017-09-03 21:20:39 +00:00
get = (path: string, data: ?Object, options?: Object) => {
return this.fetch(path, "GET", data, options);
2017-04-27 04:47:03 +00:00
};
2016-02-27 21:53:11 +00:00
2017-09-03 21:20:39 +00:00
post = (path: string, data: ?Object, options?: Object) => {
return this.fetch(path, "POST", data, options);
2017-04-27 04:47:03 +00:00
};
2016-07-23 19:09:50 +00:00
2016-02-27 21:53:11 +00:00
// Helpers
2019-03-14 06:00:41 +00:00
constructQueryString = (data: { [key: string]: string }) => {
return map(
data,
(v, k) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`
).join("&");
2016-02-27 21:53:11 +00:00
};
}
export default ApiClient;
// In case you don't want to always initiate, just import with `import { client } ...`
2017-05-30 02:08:03 +00:00
export const client = new ApiClient();