Better error handling for API

Updated error entities so that they work on an api failure (status code != 200). This works with the sagas much better to watch for FAILURE/SUCCESS. http browser errors can be avoided by checking api params.

Added timestamp for most of the fiddle calls, to allow for accurate selecting.

Add request payload to errors for better error entity selection.
This commit is contained in:
emizzle 2018-09-04 12:38:18 +10:00
parent 69aea01b8b
commit 9f264dc0d4
9 changed files with 159 additions and 141 deletions

View File

@ -179,7 +179,7 @@ export const fiddleCompile = {
export const FIDDLE_DEPLOY = createRequestTypes('FIDDLE_DEPLOY');
export const fiddleDeploy = {
post: (compiledCode) => action(FIDDLE_DEPLOY[REQUEST], {compiledCode}),
post: (compiledCode, timestamp) => action(FIDDLE_DEPLOY[REQUEST], {compiledCode, timestamp}),
success: (response) => {
return action(FIDDLE_DEPLOY[SUCCESS], {fiddleDeploys: response.result});
},
@ -188,14 +188,20 @@ export const fiddleDeploy = {
export const FIDDLE_FILE = createRequestTypes('FIDDLE_FILE');
export const fiddleFile = {
request: () => action(FIDDLE_FILE[REQUEST]),
success: (codeToCompile) => action(FIDDLE_FILE[SUCCESS], {codeToCompile}),
failure: (error) => action(FIDDLE_FILE[FAILURE], {error})
request: (timestamp) => {
return action(FIDDLE_FILE[REQUEST], {timestamp});
},
success: (codeToCompile, payload) => action(FIDDLE_FILE[SUCCESS], {fiddleFiles: [{codeToCompile, ...payload}]}),
failure: (error, payload) => {
return action(FIDDLE_FILE[FAILURE], {fiddleFiles: [{error, ...payload}]});
}
};
export const FIDDLE_PROFILE = createRequestTypes('FIDDLE_PROFILE');
export const fiddleProfile = {
post: (compiledCode, timestamp) => action(FIDDLE_PROFILE[REQUEST], {compiledCode, timestamp}),
post: (compiledCode, timestamp) => {
return action(FIDDLE_PROFILE[REQUEST], {compiledCode, timestamp});
},
success: (fiddleProfile, payload) => {
return action(FIDDLE_PROFILE[SUCCESS], {fiddleProfiles: [{...fiddleProfile, ...payload}]});
},

View File

@ -141,7 +141,7 @@ export function postFiddleCompile(payload) {
}
export function postFiddleDeploy(payload) {
return post('/contract/deploy', {compiledContract: payload.compiledCode});
return post('/contract/deploy', payload);
}
export function fetchFiles() {
@ -149,6 +149,6 @@ export function fetchFiles() {
}
export function postFiddleProfile(payload) {
return post('/contract/profiler/profile', {compiledContract: payload.compiledCode});
return post('/profiler/profile', payload);
}

View File

@ -41,7 +41,7 @@ class FiddleContainer extends Component {
componentDidMount() {
this.setState({loadingMessage: 'Loading saved state...'});
this.props.fetchLastFiddle();
this.props.fetchLastFiddle(Date.now());
}
componentDidUpdate(prevProps) {
@ -222,7 +222,7 @@ class FiddleContainer extends Component {
fatalFiddleCard={this._renderFatalCard("Failed to compile", fiddleCompileError)}
fatalFiddleDeployCard={this._renderFatalCard("Failed to deploy", fiddleDeployError)}
compiledContractsCard={compiledFiddle && compiledFiddle.compilationResult && this._renderSuccessCard("Contract(s) compiled!",
<ContractFunctions contractProfile={profiledFiddle}
profiledFiddle && <ContractFunctions contractProfile={profiledFiddle}
contractFunctions={deployedFiddle}
onlyConstructor
postContractFunction={this._onDeployClick}/>

View File

@ -1,5 +1,5 @@
import {combineReducers} from 'redux';
import {REQUEST, SUCCESS} from "../actions";
import {REQUEST, SUCCESS, FAILURE} from "../actions";
const BN_FACTOR = 10000;
const voidAddress = '0x0000000000000000000000000000000000000000';
@ -22,6 +22,7 @@ const entitiesDefaultState = {
fiddleCompiles: [],
fiddleDeploys: [],
fiddleProfiles: [],
fiddleFiles: [],
versions: [],
plugins: [],
ensRecords: [],
@ -122,7 +123,7 @@ function errorMessage(state = null, action) {
}
function errorEntities(state = {}, action) {
if (!action.type.endsWith(SUCCESS)) {
if (!action.type.endsWith(FAILURE)) {
return state;
}
let newState = {};

View File

@ -13,7 +13,7 @@ function *doRequest(entity, apiFn, payload) {
if(response) {
yield put(entity.success(response.data, payload));
} else if (error) {
yield put(entity.failure(error));
yield put(entity.failure(error, payload));
}
}
@ -264,6 +264,7 @@ export default function *root() {
fork(watchFetchContract),
fork(watchFetchTransaction),
fork(watchPostFiddleCompile),
fork(watchPostFiddleCompileSuccess),
fork(watchPostFiddleDeploy),
fork(watchPostFiddleProfile),
fork(watchFetchLastFiddle),

View File

@ -169,11 +169,11 @@ class ContractsManager {
'/embark-api/contract/deploy',
(req, res) => {
this.logger.trace(`POST request /embark-api/contract/deploy:\n ${JSON.stringify(req.body)}`);
if(typeof req.body.compiledContract !== 'object'){
return res.send({error: 'Body parameter \'compiledContract\' must be an object'});
if(typeof req.body.compiledCode !== 'object'){
return res.send({error: 'Body parameter \'compiledCode\' must be an object'});
}
self.compiledContracts = Object.assign(self.compiledContracts, req.body.compiledContract);
const contractNames = Object.keys(req.body.compiledContract);
self.compiledContracts = Object.assign(self.compiledContracts, req.body.compiledCode);
const contractNames = Object.keys(req.body.compiledCode);
self.build((err, _mgr) => {
if(err){
return res.send({error: err.message});

View File

@ -105,13 +105,14 @@ class Profiler {
'post',
'/embark-api/profiler/profile',
(req, res) => {
let contract = req.body.compiledContract;
if(!contract){
res.send({error: 'Body parameter \'compiledContract\' is required (MISSING_PARAM).'});
if (!(req.body.fiddleCompiles && req.body.fiddleCompiles[0] && req.body.fiddleCompiles[0].compilationResult)) {
return res.status(204).send(); // send emptry response
}
this.profileJSON(contract, (err, table) => {
const contract = req.body.fiddleCompiles[0].compilationResult;
if (typeof contract !== 'object') {
return res.status(422).send({error: 'Body parameter \'compiledCode\' must be a string'});
}
this.profileContract(contract, (err, table) => {
if (err) {
return res.send({error: err.message});
}

View File

@ -20,15 +20,19 @@ class Solidity {
'post',
'/embark-api/contract/compile',
(req, res) => {
if(typeof req.body.codeToCompile !== 'string'){
return res.send({error: 'Body parameter \'codeToCompile\' must be a string'});
if (!(req.body.fiddleFiles && req.body.fiddleFiles[0] && req.body.fiddleFiles[0].codeToCompile)) {
return res.status(204).send(); // send emptry response
}
const input = {'fiddle': {content: req.body.codeToCompile.replace(/\r\n/g, '\n')}};
const sourceCode = req.body.fiddleFiles[0].codeToCompile;
if (typeof sourceCode !== 'string') {
return res.status(422).send({error: 'Body parameter \'codeToCompile\' must be a string'});
}
const input = {'fiddle': {content: sourceCode.replace(/\r\n/g, '\n')}};
this.compile_solidity_code(input, {}, true, (errors, compilationResult) => {
// write code to filesystem so we can view the source after page refresh
const className = !compilationResult ? 'temp' : Object.keys(compilationResult).join('_');
this._writeFiddleToFile(req.body.codeToCompile, className, Boolean(compilationResult), (err) => {
if(err) this.logger.trace('Error writing fiddle to filesystem: ', err);
this._writeFiddleToFile(sourceCode, className, Boolean(compilationResult), (err) => {
if (err) this.logger.trace('Error writing fiddle to filesystem: ', err);
}); // async, do not need to wait
const responseData = {errors: errors, compilationResult: compilationResult};
@ -39,23 +43,23 @@ class Solidity {
);
}
_writeFiddleToFile(code, className, isCompiled, cb){
_writeFiddleToFile(code, className, isCompiled, cb) {
fs.mkdirp('.embark/fiddles', (err) => {
if(err) return cb(err);
if (err) return cb(err);
// always write to temp.sol file
const filePath = Solidity._getFiddlePath('temp');
fs.writeFile(filePath, code, 'utf8', cb);
// if it's compiled, also write to [classname].sol
if(isCompiled){
if (isCompiled) {
const filePath = Solidity._getFiddlePath(className);
fs.writeFile(filePath, code, 'utf8', cb);
}
});
}
static _getFiddlePath(className){
static _getFiddlePath(className) {
return fs.dappPath(`.embark/fiddles/${className}.sol`);
}
@ -64,7 +68,7 @@ class Solidity {
self.solcW.compile(jsonObj, function (err, output) {
self.events.emit('contracts:compile:solc', jsonObj);
if(err){
if (err) {
return callback(err);
}
if (output.errors && returnAllErrors) {
@ -72,7 +76,7 @@ class Solidity {
}
if (output.errors) {
for (let i=0; i<output.errors.length; i++) {
for (let i = 0; i < output.errors.length; i++) {
if (output.errors[i].type === 'Warning') {
self.logger.warn(output.errors[i].formattedMessage);
}
@ -156,7 +160,7 @@ class Solidity {
const className = contractName;
let filename = contractFile;
if(filename === 'fiddle') filename = Solidity._getFiddlePath(className);
if (filename === 'fiddle') filename = Solidity._getFiddlePath(className);
compiled_object[className] = {};
compiled_object[className].code = contract.evm.bytecode.object;

View File

@ -20,7 +20,7 @@ class Pipeline {
const self = this;
self.events.setCommandHandler("files:contract", (filename, cb) => {
// handle case where we have a fiddle file and not a file stored in the dapp
if(filename.indexOf('.embark/fiddles') > -1){
if (filename.indexOf('.embark/fiddles') > -1) {
return fs.readFile(filename, 'utf8', (err, source) => {
if (err) return cb({error: err});
cb(source);
@ -47,7 +47,12 @@ class Pipeline {
'/embark-api/files/lastfiddle',
(req, res) => {
fs.readFile(fs.dappPath('.embark/fiddles/temp.sol'), 'utf8', (err, source) => {
if (err) return res.send({error: err.message});
if (err) {
if (err.message.indexOf('ENOENT') > -1) {
return res.status(204).send(); // send empty response
}
return res.status(400).send({error: err.message});
}
res.send(source);
});
}
@ -79,7 +84,7 @@ class Pipeline {
}
async.waterfall([
function createPlaceholderPage(next){
function createPlaceholderPage(next) {
self.events.request('embark-building-placeholder', (html) => {
fs.mkdirpSync(self.buildDir); // create dist/ folder if not already exists
fs.writeFile(self.buildDir + 'index.html', html, next);
@ -107,7 +112,7 @@ class Pipeline {
self.events.request('contracts:list', (_err, contracts) => {
// ensure the .embark/contracts directory exists (create if not exists)
fs.mkdirp(fs.dappPath(".embark/contracts", ''), (err) => {
if(err) return next(err);
if (err) return next(err);
// Create a file .embark/contracts/index.js that requires all contract files
// Used to enable alternate import syntax:
@ -124,9 +129,9 @@ class Pipeline {
// add the contract to the exports list to support alternate import syntax
importsHelperFile.write(`"${contract.className}": require('./${contract.className}').default`);
if(idx < contracts.length - 1) importsHelperFile.write(',\n'); // add a comma if we have more contracts to add
if (idx < contracts.length - 1) importsHelperFile.write(',\n'); // add a comma if we have more contracts to add
});
}, function(){
}, function () {
importsHelperFile.write('\n}'); // close the module.exports = {}
importsHelperFile.close(next); // close the write stream
});
@ -135,110 +140,110 @@ class Pipeline {
},
function assetFileWrite(next) {
async.eachOf(self.assetFiles, function (files, targetFile, cb) {
async.map(files,
function (file, fileCb) {
self.logger.trace("reading " + file.filename);
async.map(files,
function (file, fileCb) {
self.logger.trace("reading " + file.filename);
// Not a JS file
if (file.filename.indexOf('.js') < 0) {
return file.content(function (fileContent) {
self.runPlugins(file, fileContent, fileCb);
});
}
// JS files
async.waterfall([
function runWebpack(next) {
let built = false;
const webpackProcess = new ProcessLauncher({
modulePath: utils.joinPath(__dirname, 'webpackProcess.js'),
logger: self.logger,
events: self.events,
exitCallback: function (code) {
if (!built) {
return next(`File building of ${file.filename} exited with code ${code} before the process finished`);
}
if (code) {
self.logger(__('File building process exited with code ', code));
}
}
});
webpackProcess.send({action: constants.pipeline.init, options: {env: self.env}});
webpackProcess.send({action: constants.pipeline.build, file, importsList});
webpackProcess.once('result', constants.pipeline.built, (msg) => {
built = true;
webpackProcess.kill();
return next(msg.error);
});
},
function readFile(next) {
fs.readFile('./.embark/' + file.filename, (err, data) => {
if (err) {
return next(err);
}
next(null, data.toString());
});
},
function runPluginsOnContent(fileContent, next) {
self.runPlugins(file, fileContent, next);
}
], function (err, contentFile) {
if (err) {
self.logger.error(err);
return fileCb(err);
}
fileCb(null, contentFile);
// Not a JS file
if (file.filename.indexOf('.js') < 0) {
return file.content(function (fileContent) {
self.runPlugins(file, fileContent, fileCb);
});
},
function (err, contentFiles) {
if (err) {
self.logger.error(__('errors found while generating') + ' ' + targetFile);
}
let dir = targetFile.split('/').slice(0, -1).join('/');
self.logger.trace("creating dir " + self.buildDir + dir);
fs.mkdirpSync(self.buildDir + dir);
// if it's a directory
if (targetFile.slice(-1) === '/' || targetFile.indexOf('.') === -1) {
let targetDir = targetFile;
if (targetDir.slice(-1) !== '/') {
targetDir = targetDir + '/';
}
async.each(contentFiles, function (file, mapCb) {
let filename = file.filename.replace(file.basedir + '/', '');
self.logger.info("writing file " + (self.buildDir + targetDir + filename).bold.dim);
fs.copy(file.path, self.buildDir + targetDir + filename, {overwrite: true}, mapCb);
}, cb);
return;
}
let content = contentFiles.map(function (file) {
if (file === undefined) {
return "";
}
return file.content;
}).join("\n");
self.logger.info(__("writing file") + " " + (self.buildDir + targetFile).bold.dim);
if(new RegExp(/^index.html?/i).test(targetFile)){
targetFile = targetFile.replace('index', 'index-temp');
placeholderPage = targetFile;
}
fs.writeFile(self.buildDir + targetFile, content, cb);
}
);
},
// JS files
async.waterfall([
function runWebpack(next) {
let built = false;
const webpackProcess = new ProcessLauncher({
modulePath: utils.joinPath(__dirname, 'webpackProcess.js'),
logger: self.logger,
events: self.events,
exitCallback: function (code) {
if (!built) {
return next(`File building of ${file.filename} exited with code ${code} before the process finished`);
}
if (code) {
self.logger(__('File building process exited with code ', code));
}
}
});
webpackProcess.send({action: constants.pipeline.init, options: {env: self.env}});
webpackProcess.send({action: constants.pipeline.build, file, importsList});
webpackProcess.once('result', constants.pipeline.built, (msg) => {
built = true;
webpackProcess.kill();
return next(msg.error);
});
},
function readFile(next) {
fs.readFile('./.embark/' + file.filename, (err, data) => {
if (err) {
return next(err);
}
next(null, data.toString());
});
},
function runPluginsOnContent(fileContent, next) {
self.runPlugins(file, fileContent, next);
}
], function (err, contentFile) {
if (err) {
self.logger.error(err);
return fileCb(err);
}
fileCb(null, contentFile);
});
},
function (err, contentFiles) {
if (err) {
self.logger.error(__('errors found while generating') + ' ' + targetFile);
}
let dir = targetFile.split('/').slice(0, -1).join('/');
self.logger.trace("creating dir " + self.buildDir + dir);
fs.mkdirpSync(self.buildDir + dir);
// if it's a directory
if (targetFile.slice(-1) === '/' || targetFile.indexOf('.') === -1) {
let targetDir = targetFile;
if (targetDir.slice(-1) !== '/') {
targetDir = targetDir + '/';
}
async.each(contentFiles, function (file, mapCb) {
let filename = file.filename.replace(file.basedir + '/', '');
self.logger.info("writing file " + (self.buildDir + targetDir + filename).bold.dim);
fs.copy(file.path, self.buildDir + targetDir + filename, {overwrite: true}, mapCb);
}, cb);
return;
}
let content = contentFiles.map(function (file) {
if (file === undefined) {
return "";
}
return file.content;
}).join("\n");
self.logger.info(__("writing file") + " " + (self.buildDir + targetFile).bold.dim);
if (new RegExp(/^index.html?/i).test(targetFile)) {
targetFile = targetFile.replace('index', 'index-temp');
placeholderPage = targetFile;
}
fs.writeFile(self.buildDir + targetFile, content, cb);
}
);
},
next);
},
function removePlaceholderPage(next){
function removePlaceholderPage(next) {
let placeholderFile = self.buildDir + placeholderPage;
fs.access(self.buildDir + placeholderPage, (err) => {
if (err) return next(); // index-temp doesn't exist, do nothing
@ -257,7 +262,7 @@ class Pipeline {
return fileCb(null, {content: fileContent, filename: file.filename, path: file.path, basedir: file.basedir, modified: true});
}
async.eachSeries(self.pipelinePlugins,
function(plugin, pluginCB) {
function (plugin, pluginCB) {
if (file.options && file.options.skipPipeline) {
return pluginCB();
}
@ -292,7 +297,7 @@ class Pipeline {
function writeContractsJSON(contracts, next) {
async.each(contracts, (contract, eachCb) => {
fs.writeJson(fs.dappPath(self.buildDir, 'contracts', contract.className + ".json"), contract, {spaces: 2}, eachCb);
}, () => { next(); });
}, () => {next();});
}
], cb);
}