Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix fatal error by checking for valid order before processing webhook #4287

Merged
merged 4 commits into from
May 25, 2022

Conversation

FangedParakeet
Copy link
Contributor

@FangedParakeet FangedParakeet commented May 24, 2022

Fixes #4286

Changes proposed in this Pull Request

Fixing Fatal Errors From Fractured Webhooks

Please read the details within #4286 for more context on the need for this fix.

If the event body of the process_webhook_payment_intent_succeeded webhook event is not associated with a valid order ID for whatever reason, this fix will prevent a fatal error. All other functions within WC_Payments_Webhook_Processing_Service already have similar safety nets to handle mysteriously indecipherable orders and with these changes the process_webhook_payment_intent_succeeded handler will fall in line by throwing the same Invalid_Webhook_Data_Exception if an order is not to be found.

Testing instructions

  1. Please pull the changes in this PR.
  2. We will need to trigger the webhooks REST endpoint with an event type of payment_intent.succeeded. I'm not sure if there is a better way to do this, but I managed it by making a POST request to the endpoint using Postman. The target endpoint should be /wp-json/wc/v3/payments/webhook. Here is an example of a test request body. All of these fields will be required and the object id (here pi_123) must not match anything in your orders table.
{
  "id": "evt_123",
  "object": "event",
  "data": {
    "object": {
      "id": "pi_123",
      "object": "payment_intent",
      "charge": "ch_123",
      "status": "processing",
      "invoice": "inv_123",
      "metadata": [],
      "charges": {
          "data": [
              {
                  "id": "ch_123"
              }
          ]
      }
    }
  },
  "type": "payment_intent.succeeded"
  1. If we make the above request without these changes, the response should be an internal_server_error that states that there has been a critical error on the website. However, with the changes we should now be greeted with a marginally friendlier 400 bad_request error instead.
  2. Double-check that we threw the correct Exception by checking the WCPay status/error logs. There should be an error printed similar to the following: "ERROR WCPay\Exceptions\Invalid_Webhook_Data_Exception: Could not find order via charge ID: ch_123 in /var/www/html/wp-content/plugins/woocommerce-payments/includes/class-wc-payments-webhook-processing-service.php:365".

  • Run npm run changelog to add a changelog file, choose patch to leave it empty if the change is not significant. You can add multiple changelog files in one PR by running this command a few times.
  • Covered with tests (or have a good reason not to test in description ☝️)
  • Tested on mobile (or does not apply)

Post merge

@FangedParakeet FangedParakeet marked this pull request as ready for review May 24, 2022 23:17
Comment on lines 364 to 372
if ( ! $order ) {
throw new Invalid_Webhook_Data_Exception(
sprintf(
/* translators: %1: charge ID */
__( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ),
$charge_id
)
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking into code it looks like the bug is caused by this line, according to it missing order is valid case and execution should continue, rather than throw exception. However, this change stops execution and throws the exception.

@htdat looks like you were the author of get_order_from_event_body_intent_id (code), could you please clarify how the code is supposed to behave in fallback case.

Side note, this the second time when I see that the fatals are caused by mixed return type from the method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the bug comes from this commit where $order is used without checking it.

get_order_from_event_body_intent_id was created here, but it just contains the code used in process_webhook_payment_intent_succeeded and was created to avoid duplicated code.

And as @vbelolapotkov mentions, we should not throw there, because that will prevent invoice.paid to run.

Probably the simpler approach is to just check $order before using it in process_webhook_payment_intent_succeeded. Although I don't know if that could bring any other issue. There are some new additions since I reviewed the failed payments PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! process_webhook_payment_intent_succeeded was way shorter before, there were no additional uses of $order, but now we should wrap all of them within it or do an early return.

private function process_webhook_payment_intent_succeeded( $event_body ) {
$event_data = $this->read_rest_property( $event_body, 'data' );
$event_object = $this->read_rest_property( $event_data, 'object' );
$intent_id = $this->read_rest_property( $event_object, 'id' );
$order = $this->get_order_from_event_body_intent_id( $event_body );
WC_Payments_Utils::mark_payment_completed( $order, $intent_id );
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note, this the second time when I see that the fatals are caused by mixed return type from the method.

Either you return false/null or throw exception in this case. One of the problems is that I failed to see, as a reviewer, that this function can return multiple types. This should be covered with tests where every case that can occur during the execution of the function can occur.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the bug comes from this commit where $order is used without checking it.

Agreed this point!

get_order_from_event_body_intent_id was created here, but it just contains the code used in process_webhook_payment_intent_succeeded and was created to avoid duplicated code.

But looking further, the side effect coming from this commit as it introduces false for this condition branch:

} elseif ( ! empty( $event_object['invoice'] ) ) {
// If the payment intent contains an invoice it is a WCPay Subscription-related intent and will be handled by the `invoice.paid` event.
return false;
}

The original commit ref adding this condition means to return early and ignore this webhook:

} elseif ( ! empty( $event_object['invoice'] ) ) {
// If the payment intent contains an invoice it is a WCPay Subscription-related intent and will be handled by the `invoice.paid` event.
return;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But looking further, the side effect coming from this commit as it introduces false for this condition branch:

I prefer null over false for those cases. But in the end it will throw an error if we don't check if $order is valid, right? 🤔

Copy link
Member

@htdat htdat May 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ismaeldcom - yes, returning null is ideal. But in this specific case, it just introduces another type for get_order_from_event_body_intent_id as in the body of this function, it's already having: $this->wcpay_db->order_from_intent_id() and $this->wcpay_db->order_from_order_id(), which are returning these types: boolean|WC_Order|WC_Order_Refund.

For this specific issue, I think keeping the change is as light as possible, i.e. my suggestion above. For ideal return types, we may create an issue for it, and much better have a clear RFC/guideline to avoid this kind of issue in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I was thinking on the first cause of the issue, but for fixing it now, I'd stick with an early check 🙂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this commit 1c26bbd

I will take care of creating another issue later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this specific issue, I think keeping the change is as light as possible, i.e. my suggestion above. For ideal return types, we may create an issue for it, and much better have a clear RFC/guideline to avoid this kind of issue in the future.

FYI. Added the issue here #4293

@@ -361,6 +361,15 @@ private function process_webhook_payment_intent_succeeded( $event_body ) {
$charges_data = $this->read_webhook_property( $event_charges, 'data' );
$charge_id = $this->read_webhook_property( $charges_data[0], 'id' );

if ( ! $order ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move order fetching at the begging of the function, and use this if statement right after it to prevent getting all this data for nothing if the order is false

@htdat
Copy link
Member

htdat commented May 25, 2022

Updated the PR #4287 (comment)

Can someone give a final review before I merge it?

Copy link
Contributor

@ismaeldcom ismaeldcom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for taking care of it 🙂

@htdat htdat merged commit 3fb6a6d into develop May 25, 2022
@htdat htdat deleted the fix/4286-fix-webhook-service-fatal-error branch May 25, 2022 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Fatal error when webhook processing service cannot find order
5 participants