Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

HTTP::Tiny losing headers for Stripe

by Bod (Priest)
on Jun 25, 2022 at 20:32 UTC ( #11145043=perlquestion: print w/replies, xml ) Need Help??

Bod has asked for the wisdom of the Perl Monks concerning the following question:

I'm trying to update a subscription in Stripe.

This involves calling an API with an authorisation header using POST. If the payload is empty, the API returns a JSON object representing the existing subscription. If there is subscription data in the payload, Stripe attempts to update the subscription and returns the complete subscription object. Pretty straightforward and it all works fine. Until I need to read the existing subscription object and then update it.

I have hit a problem and I can't think of how to debug it further!
Here is the minimum code to demonstrate the problem:

#!/usr/bin/perl -T use CGI::Carp qw(fatalsToBrowser); use FindBin qw($RealBin); my $safepath; BEGIN { if ($RealBin =~ m!^(/home/...path.../(test|uk)/www)!) { $safepath = "$1/../lib"; } else { die "Illegal use of software - visit www.way-finder.uk to use +this site"; } } use lib "$safepath"; use Site::Variables; use HTTP::Tiny; use JSON; use Data::Dumper; use strict; use warnings; my $http = HTTP::Tiny->new; my $headers = { 'headers' => { 'Authorization' => 'Bearer ' . $Site::Variables::stripe_secret +, }, 'agent' => 'Wayfinder/v3.0', }; my $sub_id = 'sub_xxxxxxxxxxx'; # This line is the culprit... my $res = $http->post_form("https://api.stripe.com/v1/subscriptions/$s +ub_id", {}, $headers); my $payload = decode_json($res->{'content'}); my $subscription = { 'items[0][id]' => 'x', 'items[0][price]' => 'some price', }; my $response = $http->post_form("https://api.stripe.com/v1/subscriptio +ns/$sub_id", $subscription, $headers); print "Content-type: text/plain\n\n"; print Dumper $response;
With the code as it is, I get an error from Stripe that I have not supplied an API key. The key is in the $headers variable. If I take out the first call to Stripe, the one with the empty payload, then the second one succeeds * so the API key is working fine in this case. But as soon as I make two calls, it fails.

Things I've tried but haven't helped:

  • Turning off taint mode
  • Creating two instances of HTTP::Tiny and making each call with a different instance
  • Creating a copy of $headers to use in the second call
  • Adding a 5 second delay between calls to Stripe
Any ideas what I can try to solve this problem?

It is as if HTTP::Tiny doesn't like making consecutive POSTs but I cannot find anything in the documentation about this.


* - Without the first call, the second call to Stripe gives an error because I haven't got the parameters right. But it doesn't complain about there not not being an API key

Replies are listed 'Best First'.
Re: HTTP::Tiny losing headers for Stripe
by hippo (Bishop) on Jun 26, 2022 at 11:43 UTC

    I would output the arguments to the second post_form call just before it occurs. It may be that something behind the scenes has messed with $headers for example, since it is a reference.

    Without the first call, the second call to Stripe gives an error because I haven't got the parameters right. But it doesn't complain about there not not being an API key

    I would also try to fix this first. This problem may be masking others and resulting in the odd response you currently see. Once you can make the second call in isolation without any error then you can be more sure of the rest of the script.


    🦛

      I would output the arguments to the second post_form call just before it occurs.

      Bingo! - Thank you very much hippo

      That explains what is going wrong...the authorization parameter is missing...

      It doesn't explain why it is going wrong.

      Using this code:

      print "Content-type: text/plain\n\n"; my $headers = { 'headers' => { 'Authorization' => 'Bearer ' . $Site::Variables::stripe_secret +, }, 'agent' => 'Wayfinder/v3.0', }; my $sub_id = 'sub_xxxxxx'; print Dumper $headers; print "\n\n--------------\n\n"; # This line is the culprit... my $res = $http->post_form("https://api.stripe.com/v1/subscriptions/$s +ub_id", {}, $headers); print Dumper $headers; print "\n\n--------------\n\n";

      I get this output:

      $VAR1 = { 'headers' => { 'Authorization' => 'Bearer sk_test_abc123' }, 'agent' => 'Wayfinder/v3.0' }; -------------- $VAR1 = { 'agent' => 'Wayfinder/v3.0' }; --------------

      Somehow the $header is being changed during the first call. I see nothing in the documentation for HTTP::Tiny to explain this.

      In the short-term I can work around the problem by recreating $header between calls to Stripe. But it's not a very good solution.

      I would also try to fix this first

      Ordinarily, I would have fixed the API call first. However, the call requires data from the first call in order to work. Hence the need for two calls to the API.

        Nothing in the HTTP::Tiny docs but it is in the code:
        # line 239 sub post_form { my ($self, $url, $data, $args) = @_; # ... delete $args->{headers};

        The request() method doesn't seem to do this deletion.

Re: HTTP::Tiny losing headers for Stripe
by tangent (Vicar) on Jun 25, 2022 at 20:55 UTC
    When you are sending the $subscription back to Stripe I imagine it needs to be encoded as JSON - from your script it looks like you are sending a perl data structure.

    You will also need to set the "content-type" header to "application/json" and use the request() method instead of post_form() as the latter only sends "application/x-www-form-urlencoded" (at least that what the docs seem to be saying)

    my $subscription = { 'items[0][id]' => 'x', 'items[0][price]' => 'some price', }; my $options = { 'headers' => { 'Authorization' => 'Bearer ' . $Site::Variables::stripe_secret +, 'content-type' => 'application/json', }, 'content' => encode_json($subscription), }; my $response = $http->request('POST', "https://api.stripe.com/v1/subsc +riptions/$sub_id", $options);
      I imagine it needs to be encoded as JSON

      No - Stripe takes key/value pairs.

      To quote the documentation:
      accepts form-encoded request bodies, returns JSON-encoded responses

      For a form-encoded request, application/x-www-form-urlencoded is required hence the use of post_form().

      The syntax of the call to Stripe works elsewhere in my code. Indeed, the call I am making works provided I don't make another call before it - it is this problem I want help resolving.

      I'm unsure if it is an issue with HTTP::Tiny not being able to reuse the header information or an issue with Stripe or something else...

      Out of interest, I have tried using the request() method as you suggested.
      With the first call still in the code I continue to get a 401 error and the message that I have not supplied an API Key.
      If I comment out the first call to Stripe, it accepts the API Key but returns the entire subscription object because it is not accepting the JSON encoded payload.

        No - Stripe takes key/value pairs.
        Ah, I see. Still might be an issue sending a perl hash which is not the same as key-value pairs coming from a form. You could try:
        'content' => $http->www_form_urlencode( $subscription ), # or just to try getting it to work 'content' => q|"items[0][id]"="x","items[0][price]"="some price"|,

        Update: I think you can strike the above as looking at the source for the module post_form does call www_form_urlencode on the content. I'm stumped for the moment.

Re: HTTP::Tiny losing headers for Stripe
by Anonymous Monk on Jun 26, 2022 at 11:43 UTC
    How do you make a copy of $headers?
      How do you make a copy of $headers?

      Simply by assigning it to a new variable and using that...

      my $headers = { 'headers' => { 'Authorization' => 'Bearer ' . $Site::Variables::stripe_secret +, }, 'agent' => 'Wayfinder/v3.0', }; my $head = $headers;

      UPDATE:

      Thinking about it, this only creates a copy of the reference and not a copy of the underlying hash...
      Looks like I need to utilise something like Clone instead.

        just save+restore 'headers' and its value as a ref, this is the only key being deleted, no need to clone. To deal in the longterm perhaps create your own post_form() which will restore the 'headers' key. That HTTP::Tiny behaviour is quite weird. I avoid to mess with user parameters unless i document it.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11145043]
Approved by johngg
Front-paged by Corion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others musing on the Monastery: (3)
As of 2022-12-03 03:07 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    Notices?