How can I catch a WooCommerce webhook delivery and get the response? - wordpress

I'm attempting to write some functionality to more intelligently control the way WooCommerce handles failed webhooks. Currently, it just disables the webhook after 5 failed tries. I'm trying to make it so that it makes attempts in progressively larger time intervals until it either succeeds, or finally fails after the time interval reaches one day (86400).
The problem is, I can't find a way to catch a failed webhook. I'm trying to grab the response code and webhook ID from the woocommerce_webhook_delivery hook as defined in WC_Webhook::deliver() in the WC source. The problem is... even though my test WC webhooks are firing and being delivered fine, I can't ever detect that this method is running, even though all of the methods responsible for delivering webhooks refer back to it. I tried XDebug, sticking error_logs in there in the event that they were running in the background. Still no dice. Therefore, my idea of catching the woocommerce_webhook_delivery isn't working, and I don't really know of another, easier way.
Here's what little code I have so far on this, if I can detect the response code I can handle the rest....
WC_WebhookBackoff {
protected $webhook;
protected $webhook_id;
public function __construct() {
add_action('woocommerce_webhook_delivery', [$this, 'catch_webhook_failure'], 1, 5);
}
public function hooks() {
}
public function catch_webhook_failure($http_args, $response, $duration, $arg, $id) {
$failed_hooks = get_option( '_failed_wc_webhooks', true ) ? json_decode(get_option( '_wds_failed_wc_webhooks', true ) ) : [];
error_log("This should be called.");
if ( 200 !== $response ) {
$failed_hooks = [];
$this->update_failed_hooks();
}
}
[...snip...]
And yeah, this is definitely being instantiated and the hook is definitely being registered.
For reference, here's the hook I'm looking at:
do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->get_id() );
Any help is greatly appreciated.

I wanted to do the same thing essentially (capturing information about the response to my webhooks). Here's how I did it.
add_action('woocommerce_webhook_delivery', function($http_args, $response, $duration, $arg, $webhook_id) {
// do whatever you want to here
..
}, 1, 5);
Please note that I've set priority to 1 instead of the default 10, because with 10 it didn't work (I don't have a complete understanding of WordPress hook priorities yet). 5 at the end means the hook receives 5 parameters. This way my callback gets invoked.
I had a hook setup to fire when I update products. I updated my products description and my callback function was invoked. Please note, that updating the webhook (changing the endpoint or whatever) didn't invoke the callback function (not sure why, because it sends a ping to delivery address).
For testing, I also used this filter.
apply_filters('woocommerce_webhook_deliver_async', function() {
return false;
});
This essentially bypasses WooCommerce's action scheduler, the webhook's payload will be delivered immediately instead of sitting in a queue until it gets dispatched.

Related

Symfony Return Response and then do Heavy Job

On Symfony 5, I have a controller that calls a service that does a heavy job, it updates rows on the database accordingly to what the user entries.
If user enters a thousand rows I want to return a JsonResponse to the user and then do the heavy job after and send an email.
I've tried adding this to my service based on this
if($isHeavyJob) {
$email = $this->request->get("email");
$this->eventDispatcher->addListener(KernelEvents::TERMINATE, function (Event $event) use ($userEmail) {
$this->runJob($userEmail);
});
return new JsonResponse(['message' => 'Is a Heavy Job, wait a response via email.'], 200);
}
$data = $this->runJob();
return new JsonResponse(['message' => 'Not a heavy job so here is your data.', 'data' => $data], 200);
But i'm getting
Uncaught Error: Argument 1 passed to App\Service\HeavyJob::App\Service\HeavyJob{closure}() must be an instance of Symfony\Component\EventDispatcher\Event
Do I need to create a separate event ?
Think about what you might lose consistency. If you respond to the user and after what do your heavy job, the job might fail and be not completed.
As #dbrumann mention, it better to use Messaging System. In case of a failure, you could retry your job. If you write code that is tolerated to failer, you could be sure that it will complete successfully.

How to add a pause between 2 requests in POSTMAN

I have a collection of requests in POSTMAN. I wanted to add a pause between 2 requests but I couldn't find a way to do so from reading their docs. Any idea?
UPDATE I only wanted to put a pause after one request instead of after every request in the collection.
In case someone is still looking for this - You can add delay after/before 1 of many test in a collection you can use:
setTimeout(function(){}, [number]);
where 'number' is the milliseconds. If it's added at 'Tests' it will be executed after request is send. If it's added at Pre-request Scripts it will be executed before request is send.
Using javascript's busy wait is a good hack but it makes your CPU hot and the app unresponsive. I figured out this solution using postman-echo.
Assuming you want to add a long delay between Request_A and Request_B.
First, in Request_A's test script set up an env var to mark the start.
environment.delayTimerStart = new Date();
Then, create a GET request in the creation (here called 'Delay 10s'). It makes a GET on https://postman-echo.com/delay/10 (It returns after 10s)
In its test script, add
var curDate = new Date();
if (curDate - environment.delayTimerStart < delay_time_in_sec*1000) {
postman.setNextRequest('Delay 10s');
} else {
postman.setNextRequest("Request_B");
}
In this way you can add a delay of any length.
Note: 10 sec is the maxium value that postman-echo accepts. If you just need a short delay, simply GET https://postman-echo.com/delay/[1~10].
Try something like this-
if(jsonBody.responseCode=="SUCCESS"){
setTimeout(function(){
console.log("Sleeping for 3 seconds before next request.");
}, 3000);
postman.setNextRequest("nextAPI");
}
I know two possible ways to do this
Method I
Run your request as a collection. (https://www.getpostman.com/docs/collections)
Use Newman (Postman's collection runner from command line) to run your collection with --delay flag. The delay input value is in milliseconds.
Method II
This is a nice hack which I found here https://github.com/postmanlabs/postman-app-support/issues/1038. You can add a delay function to your test script in Postman.
Just simple example, I'm sure you will be understand.
setTimeout(() => {}, 15000);
15000 it's a value in miliseconds
looking at the current documentation if you are using Postman Runner
Delay
This is the interval (in ms) between each request in your collection run.
https://www.getpostman.com/docs/postman/collection_runs/starting_a_collection_run
and if you are using newman
--delay-request [number] Specify a delay (in ms) between requests [number]
https://www.getpostman.com/docs/postman/collection_runs/command_line_integration_with_newman
If you have the standalone Postman App (supports ES7) and you are intending to test only on Postman, not on newman(which doesn't support ES7), then you can have something like this in the Pre-Request script of the Request you want to delay:
function foo() {
return (new Promise((resolve, reject) => {
setTimeout(() => {
resolve("done!"); // passing argument is optional, can just use resolve()
}, 10000) // specify the delay time in ms here..
}))
}
async function waitForMe() {
await foo().then((val) => {
console.log(val); // not required, you can just do an await without then
})
}
waitForMe();
I prefer to use an online service Postman Echo's delay endpoint (docs are here). Simply create a request that uses this service and call it between the two other requests you wish to add a delay between.
If you want to check the status of something before continuing, you can use postman.setNextRequest() in the Tests of a request to loop until something has been completed. Simply do the following:
1) Create a collection structured as:
Delay For 10 Seconds
Status Check
Continue Processing
2) In the Status Check request Tests:
if(responseBody.has("Success")) //or any other success condition
{
postman.setNextRequest('Continue Processing');
tests["Success found"] = true;
}
else
{
postman.setNextRequest('Delay For 10 Seconds');
tests["No success found"] = true;
}
You can use JavaScript setTimeout method. This will make sure that your method will be executed after given delay.
setTimeout(checkStatusCode, 500);
function checkStatusCode() {
pm.sendRequest('https://postman-echo.com/get', function (err, res) {
tests['status code should be 200']= res.code ===200;
});
}

Guzzle Pool in Symfony 2 - Dealing with future

I am trying to use Guzzle pool in Symfony 2 application. I am thinking to use it because its capability of sending concurrent request at once.
However, since its async in nature I am not sure how can I use it as service in Symfony 2. Since return are not always immediate.
For example lets say I have service called Foo in Symfony which have method like this some what.
function test()
{
$request = $client->createRequest('GET', 'http://lt/?n=0', ['future' => true]);
$client->send($request)->then(function ($response) {
return "\n".$response->getBody();
});
}
Now I invoke this service like this.
$service = $this->get('foo');
$result = $service->test();
echo $result;// does not work :( echoes out null
Is there any way to get around this problem. I really want to use Future since I need async feature.
You have to deal with promises (futures) in your application or wait for results.
About your example: firstly, you don't return anything from test() ;) Because of that you don't get any $result. As I said before, you have to choose between two different ways here: 1) wait for HTTP call inside test() method and return the result itself, or 2) return Promise immediately and deal with it in your app (through then(), otherwise() and wait() in the end).
If you choose async way for the whole app, the code might look like this:
function test()
{
return $client->getAsync('http://lt/?n=0')
->then(function ($response) {
return "\n".$response->getBody();
});
}
And later:
$service = $this->get('foo');
$promise = $service->test()->then(function ($responseString) {
echo $responseString;
});
$promise->wait(); // Here you get the output.
I slightly changed you code for Guzzle 6 (current version of Guzzle that should use in new projects).
BTW, Guzzle uses its own implementation of promises, that isn't connected with React.

Aborting navigation with Meteor iron-router

I have a (client-side) router in a Meteor app, and links using the {{pathFor}} helper.
I am setting a dirty flag in the Session when the user changes a form field, and I want to trigger a warning and allow the user to stop navigating away from the page if the flag is set, basically like an onunload handler.
I've tried to do this with:
Router.onBeforeAction(function(pause) {
var self = this;
if (!this.ready()) {
return;
}
if(Session.get('dirty')) {
if(!confirm("Are you sure you want to navigate away?")) {
pause();
}
}
});
However, whilst I get the prompt, I'm still being navigated away. That is, the pause() doesn't seem to stop the subsequent router action, whatever it is.
What am I doing wrong?
From what I can tell this isn't possible with the iron-router API. What you could do however is override the Router.go method like so (somewhere in your client code):
var go = Router.go; // cache the original Router.go method
Router.go = function () {
if(Session.get('dirty')) {
if (confirm("Are you sure you want to navigate away?")) {
go.apply(this, arguments);
}
} else {
go.apply(this, arguments);
}
};
Is it somewhere specific you want to go? There is also Router.go(routeName) which will make the page point to the given routeName. What I was going for is maybe you can just force the Router to go to the current page hence neglecting the back action.
The new behavior for iron router should make this easier because it requires a call to this.next() in the onBeforeAction hook (see iron router guide), so only call that when the session is not dirty or the user confirms the warning:
if(Session.get('dirty')) {
if(confirm("Are you sure you want to navigate away?")) {
this.next();
}
} else {
this.next();
}
I found that rediecting in stop works, and works even when you aren't changing routes via Router.go (such as by links in my application).
Here is a coffeescript implementation using a class inherited from RouteController
class MyRouteController extends RouteController
stop: ->
# Save whether you data/form is dirty or whatever state you have in
# a Session variable.
if Session.get('formIsDirty')
if !confirm('You have unsaved data. Are you sure you want to leave?')
# Redirecting to the current route stops the current navigation.
# Although, it does rerun the route, so it isn't a perfect solution.
Router.go '/my_route'
# Return here so we don't perform any more of the stop operation.
return
# Otherwise do as normal.
super
The Iron Router API doesn't offer an easy way to achieve this. There is no way to cancel an ongoing transition from an onBeforeAction hook. It has to be worked around by redirecting to the previous route.
/*
* Adds a confirmation dialogue when the current route contains unsaved changes.
*
* This is tricky because Iron Router doesn't support this out of the box, and
* the reactivity gets in the way.
* In this solution, redirecting to the current route is abused
* as a mechanism to stop the current transition, which Iron Router has no API
* for. Because the redirect would trigger the onStop hook, we keep track of
* whether to run the onStop hook or not ourselves in
* `skipConfirmationForNextTransition`.
*
* When `Session.get('formIsDirty')` returns `true`, the user will be asked
* whether he really wants to leave the route or not.
*
* Further, another confirmation is added in case the browser window is closed
* with unsaved data.
*
* This gist shows the basics of how to achieve a navigation confirmation,
* also known as canceling a route transition.
* This approach may fail if other route hooks trigger reruns of hooks reactively.
* Maybe setting `skipConfirmationForNextTransition` to `true` could help in those
* cases.
*/
Session.setDefault('formIsDirty', false)
const confirmationMessage = 'You have unsaved data. Are you sure you want to leave?'
// whether the user should confirm the navigation or not,
// set to `true` before redirecting programmatically to skip confirmation
let skipConfirmationForNextTransition = false
Router.onStop(function () {
// register dependencies immediately
const formIsDirty = Session.equals('formIsDirty', true)
// prevent duplicate execution of onStop route, because it would run again
// after the redirect
if (skipConfirmationForNextTransition) {
skipConfirmationForNextTransition = false
return
}
if (formIsDirty) {
const shouldLeave = confirm(confirmationMessage)
if (shouldLeave) {
Session.set('formIsDirty', false)
return
}
// obtain a non-reactive reference to the current route
let currentRoute
Tracker.nonreactive(function () {
currentRoute = Router.current()
})
skipConfirmationForNextTransition = true
// "cancel" the transition by redirecting to the same route
// this had to be used because Iron Router doesn't support cancling the
// current transition. `url` contains the query params and hash.
this.redirect(currentRoute.url)
return
}
})
// Bonus: confirm closing of browser window
window.addEventListener('beforeunload', event => {
if (Session.get('formIsDirty')) {
// cross-browser requries returnValue to be set, as well as an actual
// return value
event.returnValue = confirmationMessage // eslint-disable-line no-param-reassign
return confirmationMessage
}
})
An up-to-date version can be found in this gist.

Subscribing to a reactive data source using autorun

I'm trying to write a webapp using Meteor and I'm definitely failing to grok something about subscribing to published datasets. The entire app is up on github (linked to the latest commit for posterity), but I'll try to summarize below.
I have a collection called teams which is available to both client and server:
Teams = new Meteor.Collection( "teams" );
On the server, I want to publish a list of all of the teams:
Meteor.publish( "allteams", function() { ...
There's a very simple cursor which makes up this published list:
var handle = Teams.find( {} ).observeChanges({
added: function( id ) {
console.log( "New team added" );
if ( !initializing ) {
console.log( "Telling subscribers it's all change" );
self.added( "teams", id, {} );
self.ready();
}
}
});
The client subscribes to that source, and when elements are added the client will add pins to a map:
Meteor.autorun( function() {
Meteor.subscribe( "allteams", function() {
console.log( "All teams has been updated" );
// Do more stuff
}
};
When the list is initially populated the autorun runs fine, but if I add another element to the collection then the publisher method logs to say "I noticed this" but nothing happens in the subscriber.
The aim of the above is as follows:
There is a list of teams on the server which consists of a name and long/lat details
When a client connects, they receive that list of teams and they're plotted on a map
If a team is added to the list on the server side, each client is notified and a new pin appears on the map.
In terms of the app, I probably don't need pins to magically appear, but it's a useful way of learning publish and subscribe, especially when I don't get it right! Eventually, "allteams" will probably be a bit more fine-grained than just the entire list of teams, so I guess it's akin to making a view on the data.
Am I missing something completely obvious?
Edit: I worked it out and put the answer below. tl;dr I wasn't subscribing to a reactive data source at all.
It's probably not polite to answer my own question, but I worked out what I was doing wrong and I figured that other people might come across the same thing.
Simple answer is that I wasn't doing what I claimed to be doing in the title of the queston, specifically, not subscribing to a reactive data source.
Meteor.autorun( function() {
Meteor.subscribe( "allteams", function() {
console.log( "All teams has been updated" );
// Do more stuff
}
};
Here I've passed the subscribe method to autorun, but that method itself isn't a reactive data source. However, it returns something which is!
// Define a subscription
var handle = Meteor.subscribe( "foo", { onReady: function() { ... } } );
Meteor.autorun( function() {
if ( handle.ready() ) {
// Now do something every time the subscription is marked as ready
}
};
The ready method of a subscription handle is reactive, so the autorun now executes every time the published document set is updated. That leads me to further questions about the efficiency of multiple clients subscribing to a database cursor and watching for changes, but I'll come to that in another question.
If autopublish is still on, this may be the cause of your problems. Try disabling autopublish and seeing if the publish statement now works properly.

Resources