Search code examples
httpcurllibcurlput

How to change http headers for put per data point with cURL in php


Currently I'm trying to upload files via a REST API that accepts PUT. I need to provide two things: xml/json data to describe the target field, and raw data. The documentation for this operation can be found here:

http://lj.platformatyourservice.com/wiki/REST_API:record_Resource#Multipart_Operations_for_Raw_Data

If you want to skip to the question, it's near the bottom.

What I have so far:

public function uploadDocument($aContract){
    $sUrl = $this->sRestUrl."/record/contract/1523";

    $sFileName = TMP_DIR."/".$aContract['Name'];
    $rTmpFile = fopen($sFileName, "w");
    $sContents = fwrite($rTmpFile, $aContract['Content']);

    $aData = array(
        '__json_data__' => '{
            "platform":{
                "record": {
                    "contract_file": "{$aContract[\'Name\']}"
                }
            }
        }',
        'contract_file' => "@$sFileName"
    );

    $ch = curl_init($sUrl);

    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_VERBOSE, 1);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLINFO_HEADER_OUT, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: multipart/form-data;'));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $aData);

    $rResponse = curl_exec($ch);
    curl_close($ch);

    return $rResponse
}

This is almost good enough. It generates this request:

PUT https://na.longjump.com/networking/rest/record/contract/1523 HTTP/1.1
Host: username.project
Accept: */*
Cookie: project=2943572094357209345
Content-Length: 304581
Expect: 100-continue
Content-Type: multipart/form-data; boundary=----------------------------a9sd7f039h2
------------------------------0849a88a4ca4 Content-Disposition: form-data; name="__json_data__" { "platform":{ "record": { "contract_file": "{$aContract['Name']}" } } } ------------------------------0849a88a4ca4 Content-Disposition: form-data; name="contract_file"; filename="/home/username/project/tmp/document.doc" Content-Type: application/octet-stream

Then all the raw encoded binary data (which does successfully translate into a word doc).

Let me re-format the header so you can read it easer:

Content-Type: multipart/form-data; boundary=----------------------------a9sd7f039h2
------------------------------a9sd7f039h2
Content-Disposition: form-data;
name="__json_data__"
{ "platform":{ "record": { "contract_file": "{$aContract['Name']}" } } }     
------------------------------a9sd7f039h2
Content-Disposition: form-data; 
name="contract_file"; 
filename="/home/username/project/tmp/document.doc" 
Content-Type: application/octet-stream

This gets me a response of:

HTTP/1.1 400 Bad Request Server: Apache-Coyote/1.1 Cache-Control: no-cache 
Pragma: no-cache Content-Type: application/json;charset=UTF-8 P3P: CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" 
Date: Fri, 25 Jan 2013 23:49:36 GMT Transfer-Encoding: chunked Connection: close     
Connection: Transfer-Encoding {"platform": {"message": { "code": "-684", "description": "Invalid Content-Type" }}}

I think this is fine, except that I need to set a Content-Type header for the json data of Content-Type: application/json. How do you do that?

I've seen suggested doing:

$aData = array(
    '__json_data__' => '{"data":"data"};type=application/json
);

or

$aData = array(
    '__json_data__' => '{"data":"data"};Content-Type=application/json
);

But only in one place. I've tried it, and it didn't really do anything, and it's sloppy anyway. Also, I've tried http_build_query for the data, but that didn't do it for me either.

Ideas?


Solution

  • Alright. After much bogosity, I came up with a bizarre solution. Basically, I made the put request to my own server, echoed the response back to myself, then put that into the CURLOPT_POSTFIELDS. Here's the code (keep in mind it's simplified; the real code has the second bit inside a function, all within a class):

    function getRawHeader($ch){
        $sResponse = $this->executeCurl($ch);
    
        preg_match("/------------------------------(\w)+/", $sResponse, $aBoundary);
        if($aBoundary){
            $sBoundary = $aBoundary[0];
            $aResponse = explode($sBoundary, $sResponse);
            foreach($aResponse as &$sPart){
                 // Find the string that defines content-type, and add it as a header (if there is one)
                preg_match("/;Content-Type:\s([\w\/])+/", $sPart, $aContentType);
                if($aContentType){
                    $sContentType = $aContentType[0];
                    // Get that out of the request
                    $sPart = str_replace("$sContentType", "", $sPart);
                    // Trim the fluff for the content type
                    $sContentType = str_replace(";Content-Type: ", "", $sContentType);
                    // Now insert the content type into the header.
                    $sPart = str_replace(": form-data;", ": form-data; Content-Type: $sContentType;", $sPart);
                }
    
            }
            unset($sPart);
    
            return implode($sBoundary, $aResponse);
        }
        return $sResponse;
    }
    
    
    $sLongjumpUrl = $this->sRestUrl."/record/Contract_Custom/1001";
    $sLocalUrl = HTML_ROOT."/echorequest.php";
    
    $sFileName = TMP_DIR."/".$aContract['Name'];
    $rTmpFile = fopen($sFileName, "w");
    $sContents = fwrite($rTmpFile, $aContract['Content']);
    
    $aData = array(
        '__json_data__' => json_encode(
            array(
                'platform' => array(
                    'record' => array(
                        'contract_file' => $aContract['Name']
                    )
                )
            )
        ).";Content-Type: application/json", // this gets parsed manually below
        'contract_file' => "@$sFileName"
    );
    // get our raw subheaders
    $ch = $this->getCurl($sLocalUrl);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
    curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: multipart/form-data"));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $aData);
    $sResponse = $this->getRawHeader($ch);
    
    // Get us a new curl, for actually sending the data to longjump
    $ch = $this->getCurl($sLongjumpUrl);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
    curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: multipart/form-data"));
    
    // We gotta manually add a boundary consistent with the one automatically generated above.
    preg_match("/------------------------------(\w)+/", $sResponse, $aBoundary);
    $sBoundary = $aBoundary ? $aBoundary[0] : '';
    $sBoundaryMarker = $sBoundary ? "boundary=" : '';
    curl_setopt($ch, CURLOPT_POSTFIELDS, $sBoundaryMarker.preg_replace("/------------------------------(\w)+/", $sBoundary, $sResponse));
    
    $sResponse = $this->executeCurl($ch);
    

    This appears to have done the job of getting the custom headers I wanted, although I'm not very confident in it. I'm still having a problem with the api returning a blank string, with no status code, so I dunno what's up with that. But this problem has been addressed, I think.