Jump to content

How to catch PHP notice errors in AJAX requests?


bernhard
 Share

Recommended Posts

Hey everybody, hope you are doing fine!

Today I had an error that was quite hard to debug... It was an AJAX request sent via RockGrid that should trash a page. It was working fine as superuser, but not for my client. So I thought it was related to access control. Then I did some hard var_dump() and die() in the AJAX and found the problem:

SwJKVu4.png

Line 29/30 refer to a saveReady hook that obviously gets called when trashing the page. Somehow (don't know why, actually) the page has output formatting turned ON and the date conversion fails. No problem so far, a getUnformatted() solves this issue and I get a nice response:

x1fW6ac.png

But I wonder how I could make the "response" more informative when something goes wrong. At the moment it just fails with an empty ajax response. Can I somehow "catch" the notice error? There is no Exception thrown. It's just an echo'ed warning.

Thank you very much for your help!

PS:

The trash button code is simple, and you can see it here: https://github.com/BernhardBaumrock/FieldtypeRockGrid/blob/master/plugins/buttons/trash.php
And it is invoked here: https://github.com/BernhardBaumrock/FieldtypeRockGrid/blob/master/InputfieldRockGrid.module.php#L223-L241

 

Link to comment
Share on other sites

Here we go:

// Out of Band PHP Error grabber
$Errs = '';
global $Errs;
function regular_error_handler($errno, $errstr, $errfile, $errline)
	{
	global $Errs;
	$Errs .= "<li><b>Custom error:</b> [$errno] $errstr<br>Error on line $errline in $errfile</li>";
	}
set_error_handler('ProcessWire\regular_error_handler'); // assuming to operate in ProcessWire namespace

// Execute some PHP
$DateOutput = date('H:i', 'SomeBogusData');
$Zero = 0;
$Result = 1 / $Zero;

// Finally check for any out of band echo messages from PHP
if($Errs != '')
	$content .= "Execution failed with<ul>{$Errs}</ul>";

 

  • Like 2
Link to comment
Share on other sites

Thx, the problem is that this echo'ed output only appears when I do a 

var_dump($data);
die();

above this block: https://github.com/BernhardBaumrock/FieldtypeRockGrid/blob/master/InputfieldRockGrid.module.php#L243-L252

Otherwise the response fails because it's not a valid JSON (because of the prepended error messages). I guess this is only in debug mode, but I'd really be interested how to catch (thx for the hint regarding the typo ? ) such errors.

Link to comment
Share on other sites

There is no difference for regular HTML or AJAX reply. If the error happens, PHP sends that out of band string back to the client. If you are waiting for a JSON, then this message prepends the JSON reply. Catching it with that error handler allows you to put the message into you JSON object before transmission.

  • Like 2
Link to comment
Share on other sites

Hm... one of us does not understand ? 

  • I'm sending the request via ajax
  • The server executes the request and it fails
  • I get an AJAX error that breaks my batch-operation (that fires one request after each other)

It should be like this:

  • Send request
  • Request fails, so return a valid json (eg "error": "error while trying to trash page xy")
  • execute all following batches

So what I want is to catch the error before the response is sent back to the client.

Link to comment
Share on other sites

1 hour ago, bernhard said:

So what I want is to catch the error before the response is sent back to the client. 

Sure, I thought about something like this:

      $payload = $this->input->post('data');
      $func = $actions[$action][0];
      $options = $actions[$action][1];
      $data = [];
     
	  set_error_handler(...); // Setup handler

      // Execute whatever requested
      if(count($options)) $response = $func->__invoke($payload, $options);
      else $response = $func->__invoke($payload);

      if($Errs != '') // Errors happened?
         $data[] = (object)[ 'error' => $Errs ];
      else
      // log data and response
      $data[] = (object)[
        'action' => $action,
        'payload' => $payload,
        'response' => $response,
      ];

You may even want to add a try..catch around the function invocation.

I'll check later but don't see a reason why that should not work.

  • Like 2
Link to comment
Share on other sites

Works for me (tried to keep it as compact as possible):

<?php namespace ProcessWire;
/* Quick test to capture out of band ECHO messages from PHP */

class WontNameItFoo {
	public function Quote() { return('The best is yet to come...'); }
	public function Einstein() { return('E = mc²'); }
	}

$PhpErrors = '';	// This will be filled from our error handler
global $PhpErrors;
function php_error_handler($errno, $errstr, $errfile, $errline)
	{
	global $PhpErrors;
	// Add an LI element to our error string
	$PhpErrors .= "<li><b>RUNTIME ERROR #{$errno}:</b> $errstr in ".basename($errfile)."({$errline}):<br>";
	$PhpErrors .= '<pre>'.file($errfile)[$errline-1].'</pre></li>';
	}

if($this->config->ajax)	// Handling an AJAX request?
	{
	header('Content-type: application/json; charset=utf-8');	// Define response header

	set_error_handler('ProcessWire\php_error_handler');	// Register Error Handler
	try {
		// Perform some PHP processing without any kind of sanitation for testing only
		// In a productive environment proper parameter verification should be implented!
		$ajaxDate = 'Date: ' . date('l jS \of F Y h:i:s A', $input->post('timestamp'));
		$ajaxDiv = $input->post('dividend') . ' / ' . $input->post('divisor') . ' = ' . (string)($input->post('dividend') / $input->post('divisor'));
		$wnif = new WontNameItFoo();
		$func = $input->post('function');
		$ajaxFunc = $wnif->$func();	// This is brute force catching of fatal PHP errors...
		}
	catch(\Throwable $t)
		{
		php_error_handler($t->getCode(), $t->getMessage(), $t->getFile(), $t->getLine());
		// $PhpErrors .= '<li><pre>'.$t->getTraceAsString().'</pre></li>'; // in case you like to see the stack trace
		}
	
	if($PhpErrors != '')	// Errors captured from our error handler?
		$TheData = [ 'errors' => $PhpErrors ];	// Only return errors
	else
		$TheData = [	// No error, return proper result
			'ajax-result' => $ajaxResult,
			'ajax-division' => $ajaxDiv,
			'ajax-date' => $ajaxDate,
			'ajax-func' => $ajaxFunc,
		];
		
	if($input->get->callback)	// JSONP? (not used here, would require GET request, useful for cross-domain query)
		echo $input->get->callback."(".json_encode($TheData).")";
	else
		echo json_encode( $TheData );	// Regular AJAX response
	die();
	}

// Setup regular HTML content including script
// You may replace "$content =" with echo, depending on your output scenario
$content = "<div><h1>{$page->title}</h1>

<form id='the-form' action='#'>
	Timestamp: <input id='timestamp' name='timestamp' type='text' value='SomeBogusData'><button type='button' onclick='setNow(\"#timestamp\");'>Now</button><br/>
	Divide <input name='dividend' type='text' value='1234'> by <input name='divisor' type='text' value='0'><br/>
	Function <select name='function'><option value='Einstein'>The Einstein</option><option value='Quote'>Personal Quote</option><option value='FourtyTwo'>The answer to life, the universe and everything</option></select></br>
	<button type='submit'>Execute</button>
</form>

<div id='ajax-status'></div>
<div id='ajax-division'></div>
<div id='ajax-date'></div>
<div id='ajax-func'></div>
</div>

<script>
function setNow(target) { $(target).val(Math.round((new Date()).getTime() / 1000)); }
$(document).ready(function() {
	$('#ajax-status').html('Ready...');
	$('#the-form').submit(function(event) {
		event.preventDefault();	// No default action

		$.ajax({ url: '.', type: 'post', data: $( '#the-form' ).serialize() })
			.done(function( jsonData ) {
				if(jsonData['errors']) { $( '#ajax-status' ).html( 'ERROR: <ul>' + jsonData['errors'] + '</ul>' ); }
				else
					{
					$( '#ajax-status' ).html( 'SUCCESS!' );
					// console.log(jsonData);
					for (var key in jsonData) { $('#'+key).html(jsonData[key]); }	// Distribute result into DIVs
					}
				})
			.fail(function() { $( '#ajax-status' ).append( 'FAILED' );
			});
		});
	});
</script>
";

See it live: https://www.team-tofahrn.de/ajax-test/

Edited by Autofahrn
Updated with exception handler, cleanup
  • Like 3
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...