add error handling for failed requests

This commit is contained in:
hippoz 2023-07-23 05:06:07 +03:00
parent 72c9650f71
commit 00f7ca72a5
Signed by: hippoz
GPG key ID: 56C4E02A85F2FBED
9 changed files with 233 additions and 106 deletions

View file

@ -1,19 +1,19 @@
<script>
import { overlayStore } from "../../stores";
import { methods, remoteSignal } from "../../request";
import { methods, remoteSignal, responseOk } from "../../request";
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
import Modal from "./Modal.svelte";
let communityName = "";
let createButtonEnabled = true;
let response;
export let close = () => {};
const create = async () => {
createButtonEnabled = false;
const { ok } = await remoteSignal(methods.createCommunity, communityName);
if (!ok) {
overlayStore.toast("Couldn't create community");
}
close();
response = await remoteSignal(methods.createCommunity, communityName);
createButtonEnabled = true;
if (responseOk(response))
close();
};
</script>
@ -21,6 +21,8 @@
<span class="h4" slot="header">Create Community</span>
<svelte:fragment slot="content">
<RpcErrorDisplay response={response} />
<RpcErrorDisplay validationIndex={0} response={response} />
<label class="input-label">
Community Name
<input class="input full-width" minlength="1" maxlength="32" bind:value={ communityName } />

View file

@ -1,24 +1,24 @@
<script>
import { overlayStore, OverlayType } from "../../stores";
import { methods, remoteCall } from "../../request";
import { methods, remoteCall, responseOk } from "../../request";
import Modal from "./Modal.svelte";
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
let username = "";
let password = "";
let buttonsEnabled = true;
let pendingOtherOpen = false;
let response;
export let close = () => {};
const create = async () => {
buttonsEnabled = false;
const { ok } = await remoteCall(methods.createUser, username, password);
if (ok) {
response = await remoteCall(methods.createUser, username, password);
if (responseOk(response)) {
overlayStore.toast("Account created");
loginInstead();
} else {
overlayStore.toast("Couldn't create account");
buttonsEnabled = true;
return;
}
};
const loginInstead = () => {
@ -42,6 +42,9 @@
<span class="h4" slot="header">Create an Account</span>
<svelte:fragment slot="content">
<RpcErrorDisplay response={response} />
<RpcErrorDisplay validationIndex={0} response={response} />
<label class="input-label">
Username
<input class="input full-width" minlength="1" maxlength="32" bind:value={ username } />
@ -49,6 +52,7 @@
<div class="separator" />
<RpcErrorDisplay validationIndex={1} response={response} />
<label class="input-label">
Password
<input class="input full-width" minlength="8" type="password" bind:value={ password } />
@ -56,7 +60,7 @@
</svelte:fragment>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ loginInstead }">Log in instead</button>
<button class="button modal-secondary-action" on:click="{ loginInstead }" disabled="{ !buttonsEnabled }">Log in instead</button>
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !buttonsEnabled }">Create</button>
</svelte:fragment>
</Modal>

View file

@ -1,20 +1,20 @@
<script>
import { overlayStore } from "../../stores";
import { methods, remoteSignal } from "../../request";
import { methods, remoteSignal, responseOk } from "../../request";
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
import Modal from "./Modal.svelte";
let channelName = "";
let createButtonEnabled = true;
let response;
export let close = () => {};
export let community = null;
const create = async () => {
createButtonEnabled = false;
const { ok } = await remoteSignal(methods.createChannel, channelName, community.id !== -1 ? community.id : null);
if (!ok) {
overlayStore.toast("Couldn't create channel");
}
close();
response = await remoteSignal(methods.createChannel, channelName, community.id !== -1 ? community.id : null);
createButtonEnabled = true;
if (responseOk(response))
close();
};
</script>
@ -27,6 +27,8 @@
</svelte:fragment>
<svelte:fragment slot="content">
<RpcErrorDisplay response={response} />
<RpcErrorDisplay validationIndex={0} response={response} />
<label class="input-label">
Channel Name
<input class="input full-width" minlength="1" maxlength="32" bind:value={ channelName } />

View file

@ -1,27 +1,29 @@
<script>
import { overlayStore } from "../../stores";
import { methods, remoteSignal } from "../../request";
import { getMessageFromResponse, methods, remoteSignal, responseOk } from "../../request";
import Modal from "./Modal.svelte";
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
export let channel;
let channelName = channel.name;
let buttonsEnabled = true;
let response;
export let close = () => {};
const save = async () => {
buttonsEnabled = false;
const { ok } = await remoteSignal(methods.updateChannelName, channel.id, channelName);
if (!ok) {
overlayStore.toast("Couldn't edit channel");
response = await remoteSignal(methods.updateChannelName, channel.id, channelName);
buttonsEnabled = true;
if (responseOk(response)) {
close();
}
close();
};
const deleteChannel = async () => {
buttonsEnabled = false;
const { ok } = await remoteSignal(methods.deleteChannel, channel.id);
if (!ok) {
overlayStore.toast("Couldn't delete channel");
const res = await remoteSignal(methods.deleteChannel, channel.id);
if (!responseOk(res)) {
overlayStore.toast(`Couldn't delete channel: ${getMessageFromResponse(res)}`);
}
close();
};
@ -37,8 +39,10 @@
<span class="h4" slot="header">Edit Channel</span>
<svelte:fragment slot="content">
<RpcErrorDisplay response={response} />
<RpcErrorDisplay validationIndex={1} response={response} />
<label class="input-label">
Channel Name
<span>Channel Name</span>
<input class="input full-width" minlength="1" maxlength="32" bind:value={ channelName } />
</label>
</svelte:fragment>

View file

@ -1,27 +1,28 @@
<script>
import { overlayStore } from "../../stores";
import { methods, remoteSignal } from "../../request";
import { getMessageFromResponse, methods, remoteSignal, responseOk } from "../../request";
import Modal from "./Modal.svelte";
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
export let message;
let messageContent = message.content;
let buttonsEnabled = true;
let response;
export let close = () => {};
const save = async () => {
buttonsEnabled = false;
const { ok } = await remoteSignal(methods.updateMessageContent, message.id, messageContent);
if (!ok) {
overlayStore.toast("Couldn't edit message");
}
close();
response = await remoteSignal(methods.updateMessageContent, message.id, messageContent);
buttonsEnabled = true;
if (responseOk(response))
close();
};
const deleteMessage = async () => {
buttonsEnabled = false;
const { ok } = await remoteSignal(methods.deleteMessage, message.id);
if (!ok) {
overlayStore.toast("Couldn't delete message");
const res = await remoteSignal(methods.deleteMessage, message.id);
if (!responseOk(res)) {
overlayStore.toast(`Couldn't delete message: ${getMessageFromResponse(res)}`);
}
close();
};
@ -37,6 +38,8 @@
<span class="h4" slot="header">Edit Message</span>
<svelte:fragment slot="content">
<RpcErrorDisplay response={response} />
<RpcErrorDisplay validationIndex={1} response={response} />
<label class="input-label">
Content
<input class="input full-width" minlength="1" bind:value={ messageContent } />

View file

@ -1,29 +1,25 @@
<script>
import { overlayStore, OverlayType } from "../../stores";
import { remoteCall } from "../../request";
import { remoteCall, responseOk } from "../../request";
import { authWithToken } from "../../auth";
import { methods } from "../../request";
import Modal from "./Modal.svelte";
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
let username = "";
let password = "";
let buttonsEnabled = true;
let pendingOtherOpen = false;
let response;
export let close = () => {};
const login = async () => {
buttonsEnabled = false;
const { ok, json } = await remoteCall(methods.loginUser, username, password);
if (ok && json && json.token) {
authWithToken(json.token, true);
response = await remoteCall(methods.loginUser, username, password);
if (responseOk(response) && response.data && response.data.token) {
authWithToken(response.data.token, true);
} else {
if (json && json.code && json.code === 6002) { // 6002 is the code for bad login
overlayStore.toast("Invalid username or password");
} else {
overlayStore.toast("Couldn't log in");
}
buttonsEnabled = true;
return;
}
};
const createAccountInstead = () => {
@ -47,6 +43,9 @@
<span class="h4" slot="header">Welcome back!</span>
<svelte:fragment slot="content">
<RpcErrorDisplay response={response} />
<RpcErrorDisplay validationIndex={0} response={response} />
<label class="input-label">
Username
<input class="input full-width" minlength="1" maxlength="32" bind:value={ username } />
@ -54,6 +53,7 @@
<div class="separator" />
<RpcErrorDisplay validationIndex={1} response={response} />
<label class="input-label">
Password
<input class="input full-width" minlength="8" type="password" bind:value={ password } />
@ -61,7 +61,7 @@
</svelte:fragment>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ createAccountInstead }">Create an account instead</button>
<button class="button modal-secondary-action" on:click="{ createAccountInstead }" disabled="{ !buttonsEnabled }">Create an account instead</button>
<button class="button button-accent modal-primary-action" on:click="{ login }" disabled="{ !buttonsEnabled }">Log In</button>
</svelte:fragment>
</Modal>

View file

@ -0,0 +1,41 @@
<script>
import { getErrorFromResponse } from "../../request";
export let response = null;
export let validationIndex = -1;
let message = null;
$: {
const error = getErrorFromResponse(response);
if (error) {
if (validationIndex >= 0) {
if (error.validationErrors) {
const found = error.validationErrors.find(e => e.index === validationIndex);
if (found) {
message = found.msg;
}
} else {
message = null;
}
} else {
if (error.validationErrors) {
message = null;
} else {
message = error.message;
}
}
}
}
</script>
<style>
span {
color: var(--red-1);
display: block;
}
</style>
{#if message}
<span>{ message }</span>
{/if}

View file

@ -29,34 +29,71 @@ export const methods = {
getCommunityChannels: withCacheable(method(405, true)),
};
export function compatibleFetch(endpoint, options) {
if (window.fetch && typeof window.fetch === "function") {
return fetch(endpoint, options);
} else {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.addEventListener("load", () => {
resolve({
status: req.status,
ok: [200, 201, 204].includes(req.status),
json() {
return JSON.parse(req.responseText);
}
});
});
req.addEventListener("error", (e) => {
reject(e);
});
req.open(options.method || "GET", endpoint);
if (options.headers) {
for (const [header, value] of Object.entries(options.headers)) {
req.setRequestHeader(header, value);
}
}
req.send(options.body);
});
export const RPCError = {
BAD_REQUEST: { code: 6000, message: "Bad request" },
RPC_VALIDATION_ERROR: { code: 6001, message: "We couldn't validate this request" },
BAD_LOGIN_CREDENTIALS: { code: 6002, message: "Incorrect login credentials" },
BAD_AUTH: { code: 6003, message: "You're not authenticated" },
NOT_FOUND: { code: 6004, message: "Not found" },
FORBIDDEN_DUE_TO_MISSING_PERMISSIONS: { code: 6005, message: "You don't have the required permissions to perform this action" },
BAD_REQUEST_KEY: { code: 6006, message: "This request requires a special password, however, the password you provided was incorrect" },
GOT_NO_DATABASE_DATA: { code: 7001, message: "Sorry, we couldn't process this request (server expected data from database, however got none)" },
FEATURE_DISABLED: { code: 7002, message: "This feature is disabled" },
INTERNAL_ERROR: { code: 7003, message: "Sorry, we couldn't process this request (internal server error)" },
};
export const RequestStatus = {
OK: 0,
NETWORK_EXCEPTION: 1,
JSON_EXCEPTION: 2,
FAILURE_STATUS: 3,
RPC_ERROR: 4,
INVARIANT_RPC_RESPONSE_COUNT: 5,
};
export const RequestStatusToMessage = {
[RequestStatus.OK]: "",
[RequestStatus.NETWORK_EXCEPTION]: "We couldn't reach the server right now",
[RequestStatus.JSON_EXCEPTION]: "We couldn't process this request right now (server gave an invalid response, failed to parse as JSON)",
[RequestStatus.FAILURE_STATUS]: "We couldn't process this request right now (server gave a failure status code)",
[RequestStatus.RPC_ERROR]: "We couldn't process this request right now (RPC error)",
[RequestStatus.INVARIANT_RPC_RESPONSE_COUNT]: "We couldn't process this request right now (invalid RPC response)",
};
export function getErrorFromResponse(response) {
if (!response) return;
if (response.status === RequestStatus.OK) return;
console.log(response);
let message = RequestStatusToMessage[response.status];
if (!message) message = "Something went wrong (unknown request error)";
if (response.status === RequestStatus.RPC_ERROR) {
let rpcErrorMessage = Object.values(RPCError).find(({ code }) => code === response.data.code);
if (rpcErrorMessage) {
rpcErrorMessage = rpcErrorMessage.message;
} else {
rpcErrorMessage = "Something went wrong (unknown RPC error)";
}
if (response.data.code === RPCError.RPC_VALIDATION_ERROR.code) {
return { message: rpcErrorMessage, validationErrors: response.data.errors };
}
return { message: rpcErrorMessage };
}
return { message };
}
export function getMessageFromResponse(response) {
return getErrorFromResponse(response).message || "Something went wrong";
}
export function responseOk(response) {
if (response.status !== RequestStatus.OK) return false;
return true;
}
export default function doRequest(method, endpoint, auth=true, body=null) {
@ -82,32 +119,46 @@ export default function doRequest(method, endpoint, auth=true, body=null) {
};
}
}
let res;
try {
const res = await compatibleFetch(endpoint, options);
const json = res.status === 204 ? {} : await res.json();
res = await fetch(endpoint, options);
} catch(o_O) {
return resolve({
json,
ok: res.ok,
});
} catch (e) {
return resolve({
json: null,
ok: false,
status: RequestStatus.NETWORK_EXCEPTION
});
}
let json;
try {
json = res.status === 204 ? {} : await res.json();
} catch(o_O) {
return resolve({
status: RequestStatus.JSON_EXCEPTION
});
}
resolve({
data: json,
status: res.ok ? RequestStatus.OK : RequestStatus.FAILURE_STATUS
});
});
}
export async function remoteCall({methodId, requiresAuthentication, cacheable, _isSignal=false}, ...args) {
const calls = [[methodId, ...args]];
if (requiresAuthentication && gateway.authenticated && !cacheable) {
const replies = await gateway.sendRPCRequest(calls, _isSignal);
const ok = Array.isArray(replies) && !(replies[0] && replies[0].code);
if (!Array.isArray(replies) || replies.length !== 1) {
return { status: RequestStatus.INVARIANT_RPC_RESPONSE_COUNT };
}
const reply = replies[0];
return {
json: ok ? replies[0] : null,
ok
data: reply,
status: reply && reply.code ? RequestStatus.RPC_ERROR : RequestStatus.OK
};
}
@ -117,9 +168,21 @@ export async function remoteCall({methodId, requiresAuthentication, cacheable, _
} else {
response = await doRequest("POST", apiRoute("rpc"), requiresAuthentication, calls);
}
response.ok = response.ok && Array.isArray(response.json) && !(response.json[0] && response.json[0].code);
response.json = response.ok ? response.json[0] : null;
return response;
if (response.status !== RequestStatus.OK) {
return { status: response.status };
}
if (!Array.isArray(response.data) || response.data.length !== 1) {
return { status: RequestStatus.INVARIANT_RPC_RESPONSE_COUNT };
}
const reply = response.data[0];
return {
data: reply,
status: reply && reply.code ? RequestStatus.RPC_ERROR : RequestStatus.OK
};
}
export async function remoteSignal(method, ...args) {
@ -131,14 +194,22 @@ export async function remoteSignal(method, ...args) {
export async function remoteBlobUpload({methodId, requiresAuthentication, _isSignal=false}, blob) {
const calls = [[methodId, [0, blob.size]]];
if (requiresAuthentication && gateway.authenticated) {
const replies = await gateway.sendRPCRequest(calls, _isSignal, blob);
const ok = Array.isArray(replies) && !(replies[0] && replies[0].code);
if (!Array.isArray(replies) || replies.length !== 1) {
return { status: RequestStatus.INVARIANT_RPC_RESPONSE_COUNT };
}
const reply = replies[0];
return {
json: ok ? replies[0] : null,
ok
data: reply,
status: reply && reply.code ? RequestStatus.RPC_ERROR : RequestStatus.OK
};
} else {
return { json: null, ok: false };
return {
status: RequestStatus.NETWORK_EXCEPTION
};
}
}

View file

@ -1,6 +1,6 @@
import gateway, { GatewayEventType, GatewayPayloadType, GatewayPresenceStatus } from "./gateway";
import logger from "./logging";
import { methods, remoteCall, remoteSignal } from "./request";
import { getMessageFromResponse, methods, remoteCall, remoteSignal, responseOk } from "./request";
import { getItem, setItem } from "./storage";
const storeLog = logger("Store");
@ -335,17 +335,17 @@ class MessageStore extends Store {
const oldestMessage = this.value[0];
const res = await remoteCall(methods.getChannelMessages, this.channelId, null, oldestMessage ? oldestMessage.id : null);
if (res.ok) {
if (res.json.length < 1)
if (responseOk(res)) {
if (res.data.length < 1)
return;
if (beforeCommitToStore)
beforeCommitToStore(res.json);
res.json.reverse();
this.value = res.json.concat(this.value);
beforeCommitToStore(res.data);
res.data.reverse();
this.value = res.data.concat(this.value);
this._recomputeMessages();
this.updated();
} else {
overlayStore.toast("Messages failed to load");
overlayStore.toast(`Messages failed to load: ${getMessageFromResponse(res)}`);
}
}
@ -877,11 +877,11 @@ export const sendMessageAction = createAction("sendMessageAction", async ({chann
const res = await remoteSignal(methods.createChannelMessage, channelId, content, optimisticMessageId, null);
if (!res.ok) {
if (!responseOk(res)) {
messagesStoreForChannel.deleteMessage({
id: optimisticMessageId
});
overlayStore.toast("Couldn't send message");
overlayStore.toast(`Couldn't send message: ${getMessageFromResponse(res)}`);
}
});