Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update error handling for Apollo 4 #2483

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions src/components/forms/GSForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import PropTypes from "prop-types";
import React from "react";
import Form from "react-formal";
import { StyleSheet, css } from "aphrodite";
import { GraphQLRequestError } from "../../network/errors";
import { log } from "../../lib";
import withMuiTheme from "../../containers/hoc/withMuiTheme";

Expand Down Expand Up @@ -41,9 +40,7 @@ class GSForm extends React.Component {
}

handleFormError(err) {
if (err instanceof GraphQLRequestError) {
this.setState({ globalErrorMessage: err.message });
} else if (err.message) {
if (err.message) {
this.setState({ globalErrorMessage: err.message });
} else {
log.error(err);
Expand Down
49 changes: 26 additions & 23 deletions src/containers/AssignmentTexterContact.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,29 +141,32 @@ export class AssignmentTexterContact extends React.Component {
};

handleSendMessageError = e => {
// NOTE: status codes don't currently work so all errors will appear
// as "Something went wrong" keeping this code in here because
// we want to replace status codes with Apollo 2 error codes.
if (e.status === 402) {
this.goBackToTodos();
} else if (e.status === 400) {
const newState = {
snackbarError: e.message
};

if (e.message === "Your assignment has changed") {
newState.snackbarActionTitle = "Back to todos";
newState.snackbarOnClick = this.goBackToTodos;
this.setState(newState);
} else {
// opt out or send message Error
this.setState({
disabled: true,
disabledText: e.message
});
this.skipContact();
}
} else {
const error_code = e.graphQLErrors[0].code;
if (error_code === 'SENDERR_ASSIGNMENTCHANGED') {
this.setState({
snackbarError: e.message,
snackbarActionTitle: "Back to todos",
snackbarOnClick: this.goBackToTodos,
});
}
else if (error_code === 'SENDERR_OPTEDOUT') {
this.setState({
disabled: true,
disabledText: e.message,
snackbarError: e.message,
});
this.handleEditStatus('closed', false);
this.skipContact();
}
else if (error_code === 'SENDERR_OFFHOURS') {
this.setState({
disabled: true,
disabledText: e.message,
snackbarError: e.message,
});
this.skipContact();
}
else {
console.error(e);
this.setState({
disabled: true,
Expand Down
34 changes: 0 additions & 34 deletions src/network/errors.js

This file was deleted.

31 changes: 20 additions & 11 deletions src/server/api/errors.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { GraphQLError } from "graphql";
import { r, cacheableData } from "../models";

// Use this error class for errors with messages that are safe/useful
// to display to users. BE CAREFUL! Revealing unnessecary error details
// can reveal information that an attacker can exploit.
export class SpokeError extends GraphQLError {}

export function authRequired(user) {
if (!user) {
throw new GraphQLError("You must login to access that resource.");
throw new SpokeError("You must login to access that resource.", {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
}

Expand All @@ -27,11 +36,11 @@ export async function accessRequired(
role
);
if (!hasRole) {
const error = new GraphQLError(
"You are not authorized to access that resource."
);
error.code = "UNAUTHORIZED";
throw error;
throw new SpokeError("You are not authorized to access that resource.", {
extensions: {
code: 'UNAUTHORIZED',
},
});
}
}

Expand Down Expand Up @@ -73,11 +82,11 @@ export async function assignmentRequiredOrAdminRole(
roleRequired
);
if (!hasPermission) {
const error = new GraphQLError(
"You are not authorized to access that resource."
);
error.code = "UNAUTHORIZED";
throw error;
throw new SpokeError("You are not authorized to access that resource.", {
extensions: {
code: 'UNAUTHORIZED',
},
});
}
return userHasAssignment || true;
}
19 changes: 11 additions & 8 deletions src/server/api/mutations/joinOrganization.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { r, cacheableData } from "../../models";
import { hasRole } from "../../../lib";
import { getConfig } from "../lib/config";
import telemetry from "../../telemetry";
import { SpokeError } from "../errors";

const INVALID_JOIN = () => {
const error = new GraphQLError("Invalid join request");
error.code = "INVALID_JOIN";
return error;
return new GraphQLError("Invalid join request", {
extensions: {
code: 'INVALID_JOIN',
},
});
};

// eslint-disable-next-line import/prefer-default-export
Expand Down Expand Up @@ -43,11 +46,11 @@ export const joinOrganization = async (
r.knex("assignment").where("campaign_id", campaignId)
);
if (campaignTexterCount >= maxTextersPerCampaign) {
const error = new GraphQLError(
"Sorry, this campaign has too many texters already"
);
error.code = "FAILEDJOIN_TOOMANYTEXTERS";
throw error;
throw new SpokeError("Sorry, this campaign has too many texters already.", {
extensions: {
code: 'FAILEDJOIN_TOOMANYTEXTERS',
},
});
}
}
} else {
Expand Down
9 changes: 6 additions & 3 deletions src/server/api/mutations/sendMessage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GraphQLError } from "graphql";
import { SpokeError } from "../errors";

import { Message, cacheableData } from "../../models";

Expand All @@ -16,8 +16,11 @@ const JOBS_SAME_PROCESS = !!(
);

const newError = (message, code, details = {}) => {
const err = new GraphQLError(message);
err.code = code;
const err = new SpokeError(message, {
extensions: {
code: code,
},
});
if (process.env.DEBUGGING_EMAILS) {
sendEmail({
to: process.env.DEBUGGING_EMAILS.split(","),
Expand Down
12 changes: 6 additions & 6 deletions src/server/api/mutations/updateServiceVendorConfig.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GraphQLError } from "graphql";
import { SpokeError } from "../errors";
import {
getConfigKey,
getService,
Expand All @@ -19,29 +19,29 @@ export const updateServiceVendorConfig = async (
const organization = await orgCache.load(organizationId);
const configuredServiceName = orgCache.getMessageService(organization);
if (configuredServiceName !== serviceName) {
throw new GraphQLError(
throw new SpokeError(
`Can't configure ${serviceName}. It's not the configured message service`
);
}

const service = getService(serviceName);
if (!service) {
throw new GraphQLError(`${serviceName} is not a valid message service`);
throw new SpokeError(`${serviceName} is not a valid message service`);
}

const serviceConfigFunction = tryGetFunctionFromService(
serviceName,
"updateConfig"
);
if (!serviceConfigFunction) {
throw new GraphQLError(`${serviceName} does not support configuration`);
throw new SpokeError(`${serviceName} does not support configuration`);
}

let configObject;
try {
configObject = JSON.parse(config);
} catch (caught) {
throw new GraphQLError("Config is not valid JSON");
throw new SpokeError("Config is not valid JSON");
}

const configKey = getConfigKey(serviceName);
Expand Down Expand Up @@ -72,7 +72,7 @@ export const updateServiceVendorConfig = async (
console.error(
`Error updating config for ${serviceName}: ${JSON.stringify(caught)}`
);
throw new GraphQLError(caught.message);
throw new SpokeError(caught.message);
}
// TODO: put this into a transaction (so read of features record doesn't get clobbered)
const dbOrganization = await Organization.get(organizationId);
Expand Down
3 changes: 2 additions & 1 deletion src/server/api/phone.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GraphQLScalarType } from "graphql";
import { GraphQLError } from "graphql";
import { Kind } from "graphql/language";
import { SpokeError } from "./errors";

const identity = value => value;

Expand All @@ -20,7 +21,7 @@ export const GraphQLPhone = new GraphQLScalarType({
}

if (!pattern.test(ast.value)) {
throw new GraphQLError("Query error: Not a valid Phone");
throw new SpokeError("Query error: Not a valid Phone");
}

return ast.value;
Expand Down
33 changes: 16 additions & 17 deletions src/server/api/schema.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import GraphQLDate from "graphql-date";
import GraphQLJSON from "graphql-type-json";
import { GraphQLError } from "graphql";
import { SpokeError } from "./errors";
import isUrl from "is-url";
import _ from "lodash";
import { gzip, makeTree, getHighestRole } from "../../lib";
Expand Down Expand Up @@ -537,12 +538,7 @@ const rootMutations = {
.limit(1);

if (!lastMessage) {
const errorStatusAndMessage = {
status: 400,
message:
"Cannot fake a reply to a contact that has no existing thread yet"
};
throw new GraphQLError(errorStatusAndMessage);
throw new SpokeError("Cannot fake a reply to a contact that has no existing thread yet");
}

const userNumber = lastMessage.user_number;
Expand Down Expand Up @@ -1073,7 +1069,7 @@ const rootMutations = {
campaign.hasOwnProperty("contacts") &&
campaign.contacts
) {
throw new GraphQLError(
throw new SpokeError(
"Not allowed to add contacts after the campaign starts"
);
}
Expand Down Expand Up @@ -1129,7 +1125,7 @@ const rootMutations = {
authRequired(user);
const invite = await Invite.get(inviteId);
if (!invite || !invite.is_valid) {
throw new GraphQLError("That invitation is no longer valid");
throw new SpokeError("That invitation is no longer valid");
}

const newOrganization = await Organization.save({
Expand Down Expand Up @@ -1455,20 +1451,23 @@ const rootMutations = {
join_token: joinToken,
})
.first();
const INVALID_REASSIGN = () => {
const error = new GraphQLError("Invalid reassign request - organization not found");
error.code = "INVALID_REASSIGN";
return error;
};
if (!campaign) {
throw INVALID_REASSIGN();
throw new GraphQLError("Invalid reassign request - campaign not found", {
extensions: {
code: 'INVALID_REASSIGN',
},
});
}
const organization = await cacheableData.organization.load(
campaign.organization_id
);
if (!organization) {
throw INVALID_REASSIGN();
}
throw new GraphQLError("Invalid reassign request - organization not found", {
extensions: {
code: 'INVALID_REASSIGN',
},
});
}
const maxContacts = getConfig("MAX_REPLIES_PER_TEXTER", organization) ?? 200;
let d = new Date();
d.setHours(d.getHours() - 1);
Expand Down Expand Up @@ -1500,7 +1499,7 @@ const rootMutations = {
const campaign = await cacheableData.campaign.load(campaignId);
await accessRequired(user, campaign.organization_id, "ADMIN", true);
if (campaign.is_started || campaign.is_archived) {
throw new GraphQLError(
throw new SpokeError(
"Cannot import a campaign script for a campaign that is started or archived"
);
}
Expand Down
Loading