Jump to content
bernhard

How to catch PHP notice errors in AJAX requests?

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

 

Share this post


Link to post
Share on other sites

Maybe output buffering can help in this case? Just call ob_start before executing the code and finally use ob_get_contents to check for some unexpected output.

Which PHP version are you running?

  • Like 1
  • Thanks 1

Share this post


Link to post
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

Share this post


Link to post
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.

Share this post


Link to post
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

Share this post


Link to post
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.

Share this post


Link to post
Share on other sites

Typically Tracy reports on AJAX errors - if not in the AJAX debug bar, usually in the Tracy error logs. Sometimes they do seem to get through but usually they show up in the browser console's network > response tab

  • Thanks 1

Share this post


Link to post
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

Share this post


Link to post
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

Share this post


Link to post
Share on other sites

Nice one @Autofahrn

Here are the results of your script without the set_error_handler() but with Tracy handling everything instead:

aeCMNczlBa.gif.573a6933f242394e674250e1ff41924a.gif

  • Like 2

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...