13

I’ve run into a limitation in the cURL bindings for PHP. It appears there is no easy way to send the same multiple values for the same key for postfields. Most of the workarounds I have come across for this have involved creating the URL encoded post fields by hand tag=foo&tag=bar&tag=baz) instead of using the associative array version of CURLOPT_POSTFIELDS.

It seems like a pretty common thing to need to support so I feel like I must have missed something. Is this really the only way to handle multiple values for the same key?

While this workaround might be considered workable (if not really annoying), my main problem is that I need to be able to do multiple values for the same key and also support file upload. As far as I can tell, file upload more or less requires to use the associate arravy version of CURLOPT_POSTFIELDS. So I feel like I am stuck.

I have posted about this problem in more detail on the cURL PHP mailing list in the hopes that someone there has some ideas about this.

Suggestions or hints on where I can look for more information on this are greatly appreciated!

10 Answers 10

12

I ended up writing my own function to build a custom CURLOPT_POSTFIELDS string with multipart/form-data. What a pain.

function curl_setopt_custom_postfields($ch, $postfields, $headers = null) {
    // $postfields is an assoc array.
    // Creates a boundary.
    // Reads each postfields, detects which are @files, and which values are arrays
    // and dumps them into a new array (not an assoc array) so each key can exist
    // multiple times.
    // Sets content-length, content-type and sets CURLOPT_POSTFIELDS with the
    // generated body.
}

I was able to use this method like this:

curl_setopt_custom_postfields($ch, array(
    'file' => '@/path/to/file',
    'tag' => array('a', 'b', 'c'),
));

I am not certain of CURLOPT_HTTPHEADER stacks, so since this method calls it, I made certain that the function would allow for the user to specify additonal headers if needed.

I have the full code available in this blog post.

Sign up to request clarification or add additional context in comments.

5 Comments

I believe this answers my question, but I see now way to actually "answer" my question. :) Please upvote this so maybe someday someone can accept it for me or something?
you can, as of now, accept your own answers, see blog.stackoverflow.com/2009/01/accept-your-own-answers
Surprised nobody else has ever come across this, seems like something that should be part of the cURL php library! Thanks for making my life easier!
i have been stuck in this hell hole for 3 days at first i didn't event know what i was doing wrong i only understood after making a form and checking the request from dev tools its 2018 and this still has no proper solution wth besides @BeauSimensen your blog is down man
Website is down, you can see function here yeehuichan.wordpress.com/2011/08/07/…
3

If you use tag[] rather than tag for the name, PHP will generate an array for you, in other words, rather than

tag=foo&tag=bar&tag=baz

You need

tag[]=foo&tag[]=bar&tag[]=baz

Note that when urlencoded for transmission this should become

tag%5B%5D=foo&tag%5B%5D=bar&tag%5B%5D=baz

1 Comment

I know how to create arrays in forms and URLs for PHP to read. The problem is that I'm posting to a non-PHP site that does not use the [] hack. Even if it did, cURL will not accept 'tag[]' => array('a', 'b', 'c') and create the request like you are suggesting it will. This is the problem.
3
  1. Vote for PHP Bug #51634.
  2. Try @BeauSimensen's answer.
  3. Guzzle can do this. See an example below.
$client = new \GuzzleHttp\Client();
$client->request('POST', $url, [
  'multipart' => [
    [ 'name' => 'foo', 'contents' => 'bar' ],
    [ 'name' => 'foo', 'contents' => 'baz' ],
  ]
]);

Comments

2

I ran into the same issue. But I was able to solve it this way.

for($cnt = 0; $cnt < count($siteRows); $cnt++)
{
    $curlParams['site_ids['.$cnt.']'] = $siteRows[$cnt]->site_id; 
}

Works for files too:

for($cnt = 0; $cnt < count($imageRows); $cnt++)
{
    $curlParams['product_images['.$cnt.']'] = '@'.$imageRows[$cnt]->full_path;
}

1 Comment

Takes advantage of php's specific interpretation of post field names, so works only if your target is running php, but being as this is a question about writing the client side in php I suppose that's likely. It helped me, +1
2

I got it working using:

curl_setopt($ch, CURLOPT_POSTFIELDS,array('tag[0]'=>'val0','tag[1]'=>'val1'));

then $_POST results in: $_POST['tag'][0] = 'val0' and $_POST['tag'][1] = 'val1'

Comments

1

I think the established standard for multiple values in one key (or the same key) is to have it concatenated with a delimiter, such as for multiple selections of option lists in form elements. I believe this delimiter is the tab character (\t) or the pipe symbol (|).

If the keyname is terminated with [] (like tag[]), PHP will automatically convert the values into an array for your convenience.

4 Comments

I know how to get PHP to accept an array with tag[], but that is not the problem. I am posting to a non-PHP site using cURL and need to be able to send multiple values for the same key (tag), and I could not get cURL to do that, whether I specify 'tag' or 'tag[]' as the param name.
Mhmm, not sure I get this right. You're posting to so-to-say somebody elses form (which is perfectly fine) ? Doesn't the original html-form allow for multiple entries in that form-element ? If no, the receiving code will not expecting it. If yes, get wireshark and look how it is encoded, then replay
I know they accept multiple values for the same tag ( tag=a&tag=b ) and they do not use the tag[]=a&tag[]=b format. I built a Google Code automatic uploader, and I believe that site is in Python. I had to build a custom postfields to build a multipart/form-data postinfo.
Also, my description on th esolution is at the bottom of the list. I wish I could accept my own answer! Or at least get some +1's on it so that it gets moved to the top. Thanks again for your help, lImbus.
1

lImbus and paul, thank you for your input.

If I had control over the form I am posting to, I could probably find an alternate solution to this problem. However, I do not have any control over the form. And I am almost positive that the software reading the post is not PHP and does not obey the tag[] standards.

Even if it did, cURL does not seem to obey the tag[] syntax either. Basically, I tried the following and neither worked...

curl_setopt($ch, CURLOPT_POSTFIELDS, array('file' => '@/pathtofile', 'tag[]' => array('a', 'b', 'c'));

curl_setopt($ch, CURLOPT_POSTFIELDS, array('file' => '@/pathtofile', 'tag' => array('a', 'b', 'c'));

And again, I don't think that passing tag[] would work anyway as the form I am posting to is actually looking for 'tag' and not 'tag[]'.

I am really starting to get the feeling that the cURL PHP bindings really have no support for this. Which seems so surprising to me. It seems like it can do quite literally anything else, yet it is unable to do something simple like this?

Comments

0

DON'T USE GUZZLE:

# at your command line start php interactive
user@group:~:php -a
php > $arr=array('var' => array(1,2,3,4)); 
php > echo http_build_query($arr);
var%5B0%5D=1&var%5B1%5D=2&var%5B2%5D=3&var%5B3%5D=4
php > echo urldecode(http_build_query($arr));
var[0]=1&var[1]=2&var[2]=3&var[3]=4

So, you need http_build_query where you pass a hash array of key-values; your (array) variable is entered as a key with value a array instead a scalar value like 'var' => array(1,2,3,4). Now, http_build_query can format the post fields of curl command:

$fields = array('key1' => 'value1', 'var' => array(1,2,3,4));
$curlPost = \http_build_query($fields);
curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);

that's 3 lines of code! how many 1000s of code lines are in Guzzle? (*)

So far, I used curl to:

  • manage Google OAuth protocol with success

  • connect with APIs like mailgun

  • handle paypal smart buttons

that's a replacement of million of lines with some 100s!

(*): the result of http_build_query can be formatted further according your needs.

1 Comment

it worths noting that http_build_query defaults to http_build_query($fields, null, ini_get('arg_separator.output'), PHP_QUERY_RFC1738)
0

I ran into the same problem in which I had to send a parameter which has to be an array from a PHP server to another server that does not use '[]' for mixing values with the same key along with a file. In Laravel 8 I could achieve this goal with Http client (of course Http client uses guzzle). Here is a sample of my code.

Illuminate\Support\Facades\Http::attach('file', $fileContents, 'file-name')
->post('https://destination' , [['name' => 'tag', 'content' => 'foo'], ['name' => 'tag', 'content' => 'bar']])

Comments

-1

I found this answer online and want to post it here before it disappears:

http://yeehuichan.wordpress.com/2011/08/07/sending-multiple-values-with-the-same-namekey-in-curl-post/

function curl_setopt_custom_postfields($ch, $postfields, $headers = null) {
    $algos = hash_algos();
    $hashAlgo = null;
    foreach ( array('sha1', 'md5') as $preferred ) {
        if ( in_array($preferred, $algos) ) {
            $hashAlgo = $preferred;
            break;
        }
    }
    if ( $hashAlgo === null ) { list($hashAlgo) = $algos; }
    $boundary =
        '----------------------------' .
        substr(hash($hashAlgo, 'cURL-php-multiple-value-same-key-support' . microtime()), 0, 12);

    $body = array();
    $crlf = "\r\n";
    $fields = array();
    foreach ( $postfields as $key => $value ) {
        if ( is_array($value) ) {
            foreach ( $value as $v ) {
                $fields[] = array($key, $v);
            }
        } else {
            $fields[] = array($key, $value);
        }
    }
    foreach ( $fields as $field ) {
        list($key, $value) = $field;
        if ( strpos($value, '@') === 0 ) {
            preg_match('/^@(.*?)$/', $value, $matches);
            list($dummy, $filename) = $matches;
            $body[] = '--' . $boundary;
            $body[] = 'Content-Disposition: form-data; name="' . $key . '"; filename="' . basename($filename) . '"';
            $body[] = 'Content-Type: application/octet-stream';
            $body[] = '';
            $body[] = file_get_contents($filename);
        } else {
            $body[] = '--' . $boundary;
            $body[] = 'Content-Disposition: form-data; name="' . $key . '"';
            $body[] = '';
            $body[] = $value;
        }
    }
    $body[] = '--' . $boundary . '--';
    $body[] = '';
    $contentType = 'multipart/form-data; boundary=' . $boundary;
    $content = join($crlf, $body);
    $contentLength = strlen($content);

    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        'Content-Length: ' . $contentLength,
        'Expect: 100-continue',
        'Content-Type: ' . $contentType,
    ));

    curl_setopt($ch, CURLOPT_POSTFIELDS, $content);

}

And to use it:

curl_setopt_custom_postfields($ch, array(
    'file' => '@a.csv',
    'name' => array('James', 'Peter', 'Richard'),
));

1 Comment

See @BeauSimensen's answer below.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.