diff --git a/js/app/controllers/EpnTapModule.js b/js/app/controllers/EpnTapModule.js
index e6f98cd..1f2479e 100644
--- a/js/app/controllers/EpnTapModule.js
+++ b/js/app/controllers/EpnTapModule.js
@@ -130,7 +130,7 @@ Ext.define('amdaDesktop.EpnTapModule', {
 
 		loadMask.show();
 		this.servicesStore.each(function(record) {
-			// TODO: use store.load() method instead.
+			// TODO: use store.load() method instead and add 'success' and 'enable' columns in the store
 			Ext.Ajax.request({
 				url: 'php/epntap.php',
 				method: 'GET',
@@ -146,8 +146,18 @@ Ext.define('amdaDesktop.EpnTapModule', {
 					'timeMax': timeMax
 				},
 				// timeout: 3000,
-				success: this.updateNbResults,
-				failure: this.updateNbResultsFail,
+				success: function(response) {
+					var record = this.servicesStore.getById(response.request.options.params['serviceId']);
+					var responseObj = Ext.decode(response.responseText);
+					if(responseObj['success']) {
+						this.updateNbResults(responseObj, record);
+					} else {
+						this.updateNbResultsFail(response, responseObj['msg']);
+					}
+				},
+				failure: function(response) {
+					this.updateNbResultsFail(response, response.statusText);
+				},
 				scope: this
 			});
 		}, this);
@@ -156,25 +166,21 @@ Ext.define('amdaDesktop.EpnTapModule', {
 	/**
 	Update the nb_result field of the services store (see `EpnTapUI.servicesStore`), according to the field values in `serviceFilterPanel`.
 	*/
-	updateNbResults: function(response) {
-		if(response.status !== 200 || isNaN(response.responseText)) {
-			this.updateNbResultsFail(response);
-		} else {
-			var record = this.servicesStore.getById(response.request.options.params['serviceId']);
-			var nbRes = Number(response.responseText);
-			record.set('nb_results', response.responseText);
-			record.set('error', '');
-			record.set('info', 'The service returned ' + (nbRes == 0 ? 'any' : nbRes) + ' result' + (nbRes > 1 ? 's' : '') + ' for the given query.');
-			this.servicesStore.sort();
-		}
+	updateNbResults: function(responseObj, record) {
+		record.set('nb_results', responseObj['data']);
+		record.set('info', responseObj['msg']);
+		record.set('error', '');
+		this.servicesStore.sort();
 		loadMask.hide();
 	},
 
-	updateNbResultsFail: function(response) {
-		var record = this.servicesStore.getById(response.request.options.params['serviceId']);
-		var reason = response.status === 200 ? response.responseText : response.statusText;
-		record.set('error', reason);
+	updateNbResultsFail: function(response, reason) {
+		var serviceId = response.request.options.params['serviceId'];
+		var record = this.servicesStore.getById(serviceId);
+		console.log('Can not get nb results for service ' + serviceId + ': ' + reason, response);
 		record.set('info', '');
+		record.set('error', reason);
+		this.servicesStore.sort();
 		loadMask.hide();
 	},
 
@@ -203,9 +209,11 @@ Ext.define('amdaDesktop.EpnTapModule', {
 		// Ext.Ajax.abortAll();
 		this.selectedService = record;
 		var nbRes = this.selectedService.get('nb_results');
-		if(nbRes > 0 && !isNaN(nbRes)) {
+
+		if(nbRes > 0 && !isNaN(nbRes)) { // TODO replace !isNaN(nbRes) by this.selectedService.get('success')
 			this.granulesStore.load({
 				params: this.getGranuleParams(),
+				// callback: function (records, operation) { console.log(Ext.decode(operation.response.responseText)); },
 				start: 0,
 				limit: this.granulesStore.pageSize,
 				scope: this
diff --git a/js/app/views/EpnTapUI.js b/js/app/views/EpnTapUI.js
index 016ea3e..47a7111 100644
--- a/js/app/views/EpnTapUI.js
+++ b/js/app/views/EpnTapUI.js
@@ -48,6 +48,8 @@ Ext.create('Ext.data.Store', {
 	]
 });
 
+// TODO: Create a generic epntap stop from which all other stores inherits.
+
 /**
 `targetNamesStore`: An ExtJS Store containing the list of the different target names defined on all granules, on
 all available EPN-TAP services (defined in `generic_data/EpnTapData/metadata.json`, updated periodically with a cron
@@ -65,7 +67,12 @@ Ext.create('Ext.data.Store', {
 	proxy: {
 		type: 'ajax',
 		url: 'php/epntap.php',
-		extraParams : { action: 'resolver' }
+		extraParams: { action: 'resolver' }
+		// listeners: {
+		// 	exception: function(proxy, response, operation) {
+		// 		console.log('Error ', response); //TODO: Use ExtJs alert instead
+		// 	}
+		// }
 	}
 });
 
@@ -140,7 +147,7 @@ selected.
 */
 Ext.create('Ext.data.Store', {
 	id: 'granulesStore',
-	model: 'granulesModel',
+	model: 'granulesModel', // Created dynamically
 	autoload: false,
 	pageSize: 25,
 	proxy: {
diff --git a/php/epntap.php b/php/epntap.php
index 2fe0892..6e84db2 100644
--- a/php/epntap.php
+++ b/php/epntap.php
@@ -1,43 +1,76 @@
 <?php
 
 include(realpath(dirname(__FILE__) . "/config.php"));
-// include(CLASSPATH . "EpnTapMgr.php");
 include(CLASSPATH . "VOTableMgr.php");
 
 $action = preg_replace("/[^a-zA-Z]+/", "", filter_var($_GET['action'], FILTER_SANITIZE_STRING));
 
 switch ($action) {
 	case 'resolver':
-		$response = json_encode(resolver());
+		$response = resolver();
 		break;
 	case 'getServices':
-		$response = json_encode(getServices());
+		$response = getServices();
 		break;
 	case 'getNbResults':
 		$response = getNbResults();
 		break;
 	case 'getGranules':
-		$response = json_encode(getGranules());
+		$response = getGranules();
 		break;
 	default:
-		$response = 'unknown action';
+		$response = ['success' => false, 'msg' => 'Unknown action: ' . $action];
 		break;
 }
-// error_log('epntap response: ' . $response);
-echo $response;
+echo json_encode($response);
 
 function resolver() {
 	$input = filter_var($_GET['input'], FILTER_SANITIZE_URL);
 	$resolver_url = "http://voparis-registry.obspm.fr/ssodnet/1/autocomplete?q=%22$input%22";
-	$result = json_decode(file_get_contents($resolver_url), true);
 
-	$targets = array();
-	foreach($result['hits'] as $e) {
-		$aliases = '<li>' . join('</li><li>', $e['aliases']) . '</li>';
-		$target = array('name' => $e['name'], 'type' => $e['type'], 'parent' => $e['parent'], 'aliases' => $aliases);
-		array_push($targets, $target);
+	$response = ['success' => true, 'metaData' => ['root' => 'data', 'messageProperty' => 'msg']];
+	try {
+		$content = file_get_contents($resolver_url);
+	} catch (Exception $e) {
+		error_log('Resolver access error: ' . $e);
+		$response['success'] = false;
+		$response['msg'] = "Resolver unreachable on $resolver_url.";
 	}
-	return $targets;
+	try {
+		$result = json_decode($content, true);
+		$targets = array();
+		foreach($result['hits'] as $e) {
+			$aliases = '<li>' . join('</li><li>', $e['aliases']) . '</li>';
+			$target = array('name' => $e['name'], 'type' => $e['type'], 'parent' => $e['parent'], 'aliases' => $aliases);
+			array_push($targets, $target);
+		}
+		$response['data'] = $targets;
+	} catch (Exception $e) {
+		error_log('Resolver type error: ' . $e);
+		$response['success'] = false;
+		$response['msg'] = 'The resolver returned a bad result.';
+	}
+	return $response;
+}
+
+function request($access_url, $query) {
+	$votMgr = new VOTableMgr;
+	$params = 'FORMAT=votable&LANG=ADQL&REQUEST=doQuery';
+	$url = $access_url . '/sync?' . $params . '&QUERY=' . urlencode(preg_replace('/\s+/', ' ', $query)); // remove also multiple whitespaces
+
+	$votMgr->load($url);
+	$data = $votMgr->parseStream();
+	$error = $votMgr->getVotableError();
+
+	$response = ['query' => $query, 'metaData' => ['root' => 'data', 'messageProperty' => 'msg']];
+	if($error) {
+		$response['success'] = false;
+		$response['msg'] = $error;
+	} else {
+		$response['success'] = true;
+		$response['data'] = $data;
+	}
+	return $response;
 }
 
 /* Return the list of available services by querying some usual registries. */
@@ -48,29 +81,33 @@ function getServices() {
 			NATURAL JOIN rr.res_schema NATURAL JOIN rr.res_table NATURAL JOIN rr.interface NATURAL JOIN rr.res_detail NATURAL JOIN rr.capability
 			WHERE standard_id='ivo://ivoa.net/std/tap' AND intf_type='vs:paramhttp' AND detail_xpath='/capability/dataModel/@ivo-id'
 			AND 1=ivo_nocasematch(detail_value, 'ivo://vopdc.obspm/std/EpnCore%') AND table_name LIKE '%.epn_core' ORDER BY short_name, table_name";
-	// error_log('getServices query: ' . $query);
-
-	for($i=0; $i<count($registriesURL); $i++) {
-		$services = request($registriesURL[$i], $query);
-		if(! array_key_exists("error", $services)) {
-			for($j=0; $j<count($services); $j++) {
-				$services[$j]['id'] = generateServiceId($services[$j]);
-				$services[$j]['nb_results'] = -1;
-				$services[$j]['info'] = 'Please make a query first.';
-				if($services[$j]['id'] == 'cdpp/amda/amdadb') {
-					array_splice($services, $j, 1);
+
+	$regNumber = 0;
+	for(; $regNumber<count($registriesURL) ; $regNumber++) {
+		$response = request($registriesURL[$regNumber], $query);
+		if($response['success']) {
+			// Add several other parameters and remove AMDA
+			for($j=0 ; $j<count($response['data']) ; $j++) {
+				$response['data'][$j]['id'] = generateServiceId($response['data'][$j]);
+				$response['data'][$j]['nb_results'] = -1;
+				$response['data'][$j]['info'] = 'Please make a query.';
+				if($response['data'][$j]['id'] == 'cdpp/amda/amdadb') {
+					array_splice($response['data'], $j, 1);
 					$j-=1;
 				}
 			}
-			return $services;
-		} else {
-			error_log('getServices error: ' . $services['error']);
-			if($i === count($registriesURL)-1) {
-				error_log("Can not access any of these registries : " . implode(', ', $registriesURL) . ", check the internet connexion.");
-				return;
+			if(isset($lastErrorMesage)) {
+				$response['msg'] = $lastErrorMesage;
 			}
+			break;
+		} else {
+			$lastErrorMesage = 'Last tried registry (' . $registriesURL[$regNumber] . ') returned this error: ' . $response['msg'] . '.';
 		}
 	}
+	if(!$response['success']) {
+		$response['msg'] = 'Can not access any of these registries: ' . implode(', ', $registriesURL) . ', last error message is ' . $lastErrorMesage;
+	}
+	return $response;
 }
 
 function getNbResults() {
@@ -82,27 +119,33 @@ function getNbResults() {
 	$timeMax = filter_var($_GET['timeMax'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
 
 	$query = "SELECT COUNT(*) AS nb_rows FROM $tableName" . createFilter($targetName, $productTypes, $timeMin, $timeMax);
-	// error_log('getNbResults query: ' . $query);
-	$result = request($url, $query);
-	if(count($result) < 1) {
-		return 'Too few returned raws.';
-	} else if(count($result) > 1) {
-		return 'Too many returned raws.';
-	} else if(!array_key_exists(0, $result)) {
-		return 'cant find raw item 0';
-	} else if(is_null($result[0])) {
-		return 'The returned raw is null.';
-	} else if(!array_key_exists("nb_rows", $result[0])) {
-		return 'cant find nb_rows.';
-	} else if(!is_numeric($result[0]['nb_rows'])) {
-		return 'The returned value is not a number.';
-	} else {
-		return (int)($result[0]['nb_rows']);
+	$response = request($url, $query);
+	if($response['success']) {
+		$response['success'] = false;
+		$response['msg'] = 'The service returned a bad value, can not get the number of results.';
+		if(count($response['data']) < 1) {
+			error_log('getNbResults error: Too few returned raws.');
+		} else if(count($response['data']) > 1) {
+			error_log('getNbResults error: Too many returned raws.');
+		} else if(!array_key_exists(0, $response['data'])) {
+			error_log('getNbResults error: cant find raw item 0');
+		} else if(is_null($response['data'][0])) {
+			error_log('getNbResults error: The returned raw is null.');
+		} else if(!array_key_exists("nb_rows", $response['data'][0])) {
+			error_log('getNbResults error: cant find nb_rows.');
+		} else if(!is_numeric($response['data'][0]['nb_rows'])) {
+			error_log('getNbResults error: The returned value is not a number.');
+		} else {
+			$response['success'] = true;
+			$response['data'] = (int)($response['data'][0]['nb_rows']);
+			$response['msg'] = 'The service returned ' . ($response['data'] == 0 ? 'no' : $response['data']) . ' result' . ($response['data'] > 1 ? 's' : '') . ' for the given query.';
+		}
 	}
+	return $response;
 }
 
 function getGranules() {
-	// error_log('getGranules GET: ' . json_encode($_GET));
+	// TODO: simplify this
 	$url = filter_var($_GET['url'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
 	$tableName = filter_var($_GET['tableName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
 	$targetName = filter_var($_GET['targetName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
@@ -116,42 +159,38 @@ function getGranules() {
 	$filter = createFilter($targetName, $productTypes, $timeMin, $timeMax);
 	$query = "SELECT TOP $limit * FROM $tableName $filter OFFSET $start";
 	// error_log('getGranules query: ' . $query);
-	$rows = request($url, $query);
-
-	$visibleColumns = ['granule_uid', 'dataproduct_type', 'time_min', 'time_max', 'access_estsize', 'thumbnail_url', 'access_url'];
-	$names = ['dataproduct_type' => 'Type', 'access_estsize' => 'Size'];
-	$renderers = ['dataproduct_type' => 'type', 'time_min' => 'date', 'time_max' => 'date', 'access_estsize' => 'size', 'thumbnail_url' => 'img', 'access_url' => 'link', 'access_format' => 'format'];
-	$flexs = ['granule_uid' => 2];
-
-	$fields = array();
-	$columns = array();
-	foreach($rows[0] as $key => $value) {
-		$fields[] = ['name' => $key, 'type' => 'string'];
-		$columns[] = [
-			'dataIndex' => $key,
-			'text' => array_key_exists($key, $names) ? $names[$key] : ucfirst(str_replace('_', ' ', $key)),
-			'flex' => array_key_exists($key, $flexs) ? $flexs[$key] : 1,
-			'hidden' => !in_array($key, $visibleColumns),
-			'renderer' => array_key_exists($key, $renderers) ? $renderers[$key] : 'text'
-		];
-	}
+	$response = request($url, $query);
+	if($response['success']) {
+		$visibleColumns = ['granule_uid', 'dataproduct_type', 'time_min', 'time_max', 'access_estsize', 'thumbnail_url', 'access_url']; // rest are hidden
+		$names = ['dataproduct_type' => 'Type', 'access_estsize' => 'Size']; // default: pretty printed key name
+		$renderers = ['dataproduct_type' => 'type', 'time_min' => 'date', 'time_max' => 'date', 'access_estsize' => 'size', 'thumbnail_url' => 'img', 'access_url' => 'link', 'access_format' => 'format']; // default: text
+		$flexs = ['granule_uid' => 2]; // default: 1
+		// $types = ['boolean' => , 'integer']; // TODO see http://php.net/manual/fr/function.gettype.php
+
+		$fields = array();
+		$columns = array();
+		foreach($response['data'][0] as $key => $value) {
+			error_log('Granule ' . $key . ' is ' . gettype($value));
+			$fields[] = ['name' => $key, 'type' => 'string'];
+			$columns[] = [
+				'dataIndex' => $key,
+				'text' => array_key_exists($key, $names) ? $names[$key] : ucfirst(str_replace('_', ' ', $key)),
+				'flex' => array_key_exists($key, $flexs) ? $flexs[$key] : 1,
+				'hidden' => !in_array($key, $visibleColumns),
+				// 'type' => array_key_exists(gettype(), $types),
+				'renderer' => array_key_exists($key, $renderers) ? $renderers[$key] : 'text'
+			];
+		}
 
-	$metadata = ['fields' => $fields, 'columns' => $columns, 'root' => 'data'];
-	return ['data' => $rows, 'total' => $nbRes, 'metaData' => $metadata];
+		$response['total'] = $nbRes;
+		$response['metaData']['fields'] = $fields;
+		$response['metaData']['columns'] = $columns;
+	}
+	return $response;
 }
 
 // ----- utils -----
 
-function request($access_url, $query) {
-	$votMgr = new VOTableMgr;
-	$params = 'FORMAT=votable&LANG=ADQL&REQUEST=doQuery';
-	$url = $access_url . '/sync?' . $params . '&QUERY=' . urlencode(preg_replace('/\s+/', ' ', $query)); // remove also multiple whitespaces
-
-	$votMgr->load($url);
-	$result = $votMgr->parseStream();
-	return $votMgr->getVotableError() ? array('error' => $votMgr->getVotableError()) : $result;
-}
-
 function createFilter($targetName, $productTypes, $timeMin, $timeMax) {
 	$filter = array();
 	if($targetName) {
--
libgit2 0.21.2