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

Detach Payment Method cause error. #1817

Open
neleid opened this issue Feb 17, 2025 · 13 comments
Open

Detach Payment Method cause error. #1817

neleid opened this issue Feb 17, 2025 · 13 comments
Labels

Comments

@neleid
Copy link

neleid commented Feb 17, 2025

Describe the bug

I'm trying to detach payment method using stripe/stripe-php library.
But, detach payment method api call fails and can not detach.

I'm confusing because,

  • I could detach cards on stripe dash board.
  • I could detach cards by using API Explorer on stripe docs page.
  • I cound detach cards by the exact same code on other stripe account - but call api from different server.(This is the most confusing)

I called api with specific api version '2025-01-27.acacia' with stripe-php v16.5.1.
I have no idea why this is happened.

Do you have any idea? Do any account settings cause this?

To Reproduce

The code to call api is almost like below.

$payment_methods = $stripe->paymentMethods->all( [
	'customer' => $stripe_customer_id,
	'type'     => 'card',
] );
if ( $payment_methods->count() == 0 ) {
	return true;
}

// detach all payment methods
foreach ( $payment_methods->data as $payment_method ) {
	// this code cause error
	$stripe->paymentMethods->detach( $payment_method->id );
}

Here is error log.

[17-Feb-2025 09:22:34 UTC] PHP Fatal error:  Uncaught Stripe\Exception\UnexpectedValueException: Invalid response body from API: <html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
 (HTTP response code was 400, json_last_error() was 4) in /home/mysite/vendor/stripe/stripe-php/lib/ApiRequestor.php:639
Stack trace:
#0 /home/mysite/vendor/stripe/stripe-php/lib/ApiRequestor.php(136): Stripe\ApiRequestor->_interpretResponse('<html>\r\n<head><...', 400, Object(Stripe\Util\CaseInsensitiveArray), 'v1')
#1 /home/mysite/vendor/stripe/stripe-php/lib/BaseStripeClient.php(184): Stripe\ApiRequestor->request('post', '/v1/payment_met...', Array, Array, 'v1', Array)
#2 /home/mysite/vendor/stripe/stripe-php/lib/Service/AbstractService.php(75): Stripe\BaseStripeClient->request('post', '/v1/payment_met...', NULL, Object(Stripe\Util\RequestOptions))
#3 /home/mysite/vendor/stripe/stripe-php/lib/Service/PaymentMethodService.php(101): Stripe\Service\AbstractService->request('post', '/v1/payment_met...', NULL, NULL)
#4 /home/mysite/classes/payments/payment-stripe.php(584): Stripe\Service\PaymentMethodService->detach('pm_1QtP5rILJ31l...')

Expected behavior

Can detach payment method.

Code snippets

OS

Linux server

PHP version

PHP 8.1.29

Library version

stripe-php v16.5.1

API version

2025-01-27.acacia

Additional context

No response

@neleid neleid added the bug label Feb 17, 2025
@neleid
Copy link
Author

neleid commented Feb 18, 2025

Update:

I checked on several situation, and found

  • Detach payment method from local development server succeeds, but fails from production server.
  • Stripe Acount is not problem. I tried to detach payment method on same Stripe account from local development server and production server, and result is one succeeded, the other failed.
  • I'm sure it's same code. Only detach method fails, list or retrieve method succeeds on both servers.

@neleid
Copy link
Author

neleid commented Feb 18, 2025

Update2:

I put print_r in request() method of BaseStripeClient.php and checked variables.

  • $path: '/v1/payment_methods/pm_1QtTk0ILJ31lMCkXPi3BNoeZ/detach'
  • $method: 'post'
  • $baseUrl: 'https://api.stripe.com'

Seems no problem, but request to api fails after this.
I also checked api key, and no problem. Besides, if api key is wrong, list() method of PaymentMethod should fail before detach().

@neleid
Copy link
Author

neleid commented Feb 18, 2025

Update3:

I think there's nothing I can do anymore.
But, I'm in hurry.

  • The Japanese law of e-commerce will take effect at the end of March.
  • I have to release application before next stripe api version with breaking change. Because there are no time to test with new version. Can you announce when it will release?

So, I have to release my application at least in first week of March. My customers are waiting for it.

@helenye-stripe
Copy link
Contributor

helenye-stripe commented Feb 18, 2025

Hello @neleid , sorry for the trouble here. Do you have request IDs for the failing requests?

The breaking change release is at end of March.

@helenye-stripe
Copy link
Contributor

Ah sorry, I realize these are nginx errors so there is no request ID. We have seen a similar issue occur before, where there are null bytes present in the data being sent to the API. Please make sure that the path, and all data sent to the API does not include a null byte or special characters.

v16.5.1 includes an improved error when a null byte is present in the URL (https://github.com/stripe/stripe-php/pull/1811/files) -- let us know if that did not catch your error!

@neleid
Copy link
Author

neleid commented Feb 19, 2025

Hi @helenye-stripe, Thank you for your reply.

I checked null bytes like below.

I put error_log() in _requestRaw() method in ApiRequestor.php of php-stripe.
As you mentioned, _requestRaw() checks if $absUrl includes null bytes, but detach request passed it's check.
So, path is not problem, I believe.

But just in case, I checked all parameters of _requestRaw().
I put check codes between $absUrl check and httpClient()->request().

for example,

if(!empty($rawHeaders)){
  _log('rawHeaders'); 
  foreach($rawHeaders as $index => $value){
    _log('    ' . $index . ' => ' . $value);
    if (false !== \strpos($value, "\0") || false !== \strpos($value, '%00')) {
      _log('!!!!! Value has null bytes !!!!!');
    }
  }
}

_log() is my utility method of error_log() which outputs only when debug mode is on.

I checked if these variables includes null bytes.

  • $method
  • $rawHeaders
  • $params
  • $apiMode

And result is

[19-Feb-2025 04:05:29 UTC] method: post
[19-Feb-2025 04:05:29 UTC] absUrl: https://api.stripe.com/v1/payment_methods/pm_1QtoxCILJ31lMCkXYjtNUGOK/detach
[19-Feb-2025 04:05:29 UTC] rawHeaders
[19-Feb-2025 04:05:29 UTC]     0 => X-Stripe-Client-User-Agent: {"httplib":"curl 7.29.0","ssllib":"NSS\/3.53.1","bindings_version":"16.5.1","lang":"php","lang_version":"8.1.29","publisher":"stripe","uname":"Linux sv13210.xserver.jp 5.4.0-205-generic #225-Ubuntu SMP Fri Jan 10 22:23:35 UTC 2025 x86_64"}
[19-Feb-2025 04:05:29 UTC]     1 => User-Agent: Stripe/v1 PhpBindings/16.5.1
[19-Feb-2025 04:05:29 UTC]     2 => Authorization: Bearer sk_test_AVExxxxxxxxxxxxxxxxxxxxxx
[19-Feb-2025 04:05:29 UTC]     3 => Stripe-Version: 2025-01-27.acacia
[19-Feb-2025 04:05:29 UTC]     4 => X-Stripe-Client-Telemetry: {"last_request_metrics":{"request_id":"req_Mz4gEQj5TDG3Yx","request_duration_ms":406,"usage":["stripe_client"]}}
[19-Feb-2025 04:05:29 UTC]     5 => Content-Type: application/x-www-form-urlencoded
[19-Feb-2025 04:05:29 UTC] hasFile: false
[19-Feb-2025 04:05:29 UTC] apiMode: v1
[19-Feb-2025 04:05:29 UTC] Invalid response body from API: <html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
 (HTTP response code was 400, json_last_error() was 4)

I changed api key with xxxxxxxxxxxxx in log.
Last error log is outputted after request().

Unfortunately, all parameters didn't includes null bytes, but api call failed. Only detach() method failed.

Do you have any idea?

@helenye-stripe
Copy link
Contributor

Can you try hardcoding a payment method ID to detach and let me know if that fails?

@neleid
Copy link
Author

neleid commented Feb 19, 2025

I put this code.

$stripe->paymentMethods->detach('pm_1QtoxCILJ31lMCkXYjtNUGOK', []);

result is,

[19-Feb-2025 07:41:01 UTC] method: post
[19-Feb-2025 07:41:01 UTC] absUrl: https://api.stripe.com/v1/payment_methods/pm_1QtoxCILJ31lMCkXYjtNUGOK/detach
[19-Feb-2025 07:41:01 UTC] rawHeaders
[19-Feb-2025 07:41:01 UTC]     0 => X-Stripe-Client-User-Agent: {"httplib":"curl 7.29.0","ssllib":"NSS\/3.53.1","bindings_version":"16.5.1","lang":"php","lang_version":"8.1.29","publisher":"stripe","uname":"Linux sv13210.xserver.jp 5.4.0-205-generic #225-Ubuntu SMP Fri Jan 10 22:23:35 UTC 2025 x86_64"}
[19-Feb-2025 07:41:01 UTC]     1 => User-Agent: Stripe/v1 PhpBindings/16.5.1
[19-Feb-2025 07:41:01 UTC]     2 => Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxxx
[19-Feb-2025 07:41:01 UTC]     3 => Stripe-Version: 2025-01-27.acacia
[19-Feb-2025 07:41:01 UTC]     4 => Content-Type: application/x-www-form-urlencoded
[19-Feb-2025 07:41:01 UTC] hasFile: false
[19-Feb-2025 07:41:01 UTC] apiMode: v1
[19-Feb-2025 07:41:01 UTC] Invalid response body from API: <html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
 (HTTP response code was 400, json_last_error() was 4)

@neleid
Copy link
Author

neleid commented Feb 19, 2025

Update:

I was just curious if call detach with parameters.
So, I put code below and run.

$stripe->paymentMethods->detach('pm_1QtoxCILJ31lMCkXYjtNUGOK', ['test_param' => true]);

Then I got,

[19-Feb-2025 11:57:55 UTC] method: post
[19-Feb-2025 11:57:55 UTC] absUrl: https://api.stripe.com/v1/payment_methods/pm_1QtoxCILJ31lMCkXYjtNUGOK/detach
[19-Feb-2025 11:57:55 UTC] rawHeaders
[19-Feb-2025 11:57:55 UTC]     0 => X-Stripe-Client-User-Agent: {"httplib":"curl 7.29.0","ssllib":"NSS\/3.53.1","bindings_version":"16.5.1","lang":"php","lang_version":"8.1.29","publisher":"stripe","uname":"Linux sv13210.xserver.jp 5.4.0-205-generic #225-Ubuntu SMP Fri Jan 10 22:23:35 UTC 2025 x86_64"}
[19-Feb-2025 11:57:55 UTC]     1 => User-Agent: Stripe/v1 PhpBindings/16.5.1
[19-Feb-2025 11:57:55 UTC]     2 => Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxx
[19-Feb-2025 11:57:55 UTC]     3 => Stripe-Version: 2025-01-27.acacia
[19-Feb-2025 11:57:55 UTC]     4 => Content-Type: application/x-www-form-urlencoded
[19-Feb-2025 11:57:55 UTC] params
[19-Feb-2025 11:57:55 UTC]     test_param => true
[19-Feb-2025 11:57:55 UTC] hasFile: false
[19-Feb-2025 11:57:55 UTC] apiMode: v1
[19-Feb-2025 11:57:56 UTC] Received unknown parameter: test_param

Of course it failed. But at least api server accepted detach call.

@neleid
Copy link
Author

neleid commented Feb 19, 2025

I checked rawHeaders of local development server, and I found some difference.

Succeeded detach call on local development server.

[19-Feb-2025 08:02:08 UTC] method: post
[19-Feb-2025 08:02:08 UTC] absUrl: https://api.stripe.com/v1/payment_methods/pm_1Qu8E2I95G9zjh8ixlYIUuQN/detach
[19-Feb-2025 08:02:08 UTC] rawHeaders
[19-Feb-2025 08:02:08 UTC]     0 => X-Stripe-Client-User-Agent: {"httplib":"curl 8.7.1","ssllib":"(SecureTransport) LibreSSL\/3.3.6","bindings_version":"16.5.1","lang":"php","lang_version":"8.1.29","publisher":"stripe","uname":"Darwin M1Max.local 24.1.0 Darwin Kernel Version 24.1.0: Thu Oct 10 21:03:15 PDT 2024; root:xnu-11215.41.3~2\/RELEASE_ARM64_T6000 arm64"}
[19-Feb-2025 08:02:08 UTC]     1 => User-Agent: Stripe/v1 PhpBindings/16.5.1
[19-Feb-2025 08:02:08 UTC]     2 => Authorization: Bearer sk_test_xxxxxxxxxxxxxxx
[19-Feb-2025 08:02:08 UTC]     3 => Stripe-Version: 2025-01-27.acacia
[19-Feb-2025 08:02:08 UTC]     4 => X-Stripe-Client-Telemetry: {"last_request_metrics":{"request_id":"req_dkJxILmmDUA6m7","request_duration_ms":218,"usage":["stripe_client"]}}
[19-Feb-2025 08:02:08 UTC]     5 => Content-Type: application/x-www-form-urlencoded
[19-Feb-2025 08:02:08 UTC] hasFile: false
[19-Feb-2025 08:02:08 UTC] apiMode: v1

Most of data is same, but curl version is different.
Curl version of production server (it is staging actually, but same as production) is older.

Do you think this might be problem?
Production server is hosting service, I can not update library, though.

@helenye-stripe
Copy link
Contributor

helenye-stripe commented Feb 19, 2025

I have three other things that you might try, though I am a bit stumped and cannot reproduce this behavior.

  1. Explicitly pass the empty params:
$stripe->paymentMethods->detach( $payment_method->id, [] );
  1. Use the method on the resource itself:
$payment_method = $stripe->paymentMethods->retrieve( <id> );
$payment_method->detach(); // or pass in empty params array
  1. Try using raw request:
$response = $stripe->rawRequest('post', '/v1/payment_methods' . $payment_method->id . '/detach'); // or pass in empty params array
$obj = $stripe->deserialize($response->body);

If those fail, I would also recommend reaching out to the developer Discord for additional advice.

It definitely seems possible that the curl version is causing issues. Would it be possible to check with your hosting provider to update the curl version?

@neleid
Copy link
Author

neleid commented Feb 20, 2025

@helenye-stripe
Thank you for your recommendations.
Unfortunately, All three solutions was failed.

I will investigate some more. especially constructRequest() method of HttpClient/CurlClient.php.

@neleid
Copy link
Author

neleid commented Feb 20, 2025

Finally, I found solution.

I added some code, then detach method has started to work.

stripe-php/lib/HttpClient/CurlClient.php

public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode = 'v1')
{
    list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode);
    
    // ====   Additional Code  Start   ==================================================
    if ( $method === 'post' && ! isset( $opts[ \CURLOPT_POSTFIELDS ] ) ) {
        $opts[ \CURLOPT_POSTFIELDS ] = '';
    }
    // ====   Additional Code  End   ====================================================
    
    list($rbody, $rcode, $rheaders) = $this->executeRequestWithRetries($opts, $absUrl);

    return [$rbody, $rcode, $rheaders];
}

CURLOPT_POSTFIELDS seems needed under a certain environment - maybe older curl versions, because $opts array will pass to curl_setopt_array() which is php function in executeRequestWithRetries() function.

So, I think if there are same method like detach will fail too. Conditions are,

  • post method.
  • no parameter is needed. or parameters are prohibited.

I think this additional code doesn't have bad effects to other method.
Maybe constructCurlOptions() method in CurlClient.php is best place to fix if Stripe-php team officially take any action.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants