6

I'm working with a rest API with token based authentication where some users have permissions to upload a file and some don't.

The problem is when some user without permission to upload a file tries to upload (Say a 1GB file), I'm getting the error response only after the whole of 1GB is uploaded.

If I copy the request as curl from chrome developers tools and send it via terminal it fails immediately.

I tested the curl command with the token of user who has permissions to upload, it works as expected.

So, How is curl different from XHR?

Curl is synchronous, and XHR is not by default. I tried making XHR synchronous, but it still has to upload the whole file before it got a response.

function upload(file, url) {
    var xhr = new XMLHttpRequest();
    xhr.upload.key = xhr.key = file.key
    xhr.upload.addEventListener("progress", updateProgress);
    xhr.addEventListener("error", transferFailed);
    xhr.addEventListener("abort", transferCanceled);
    xhr.open("PUT", url);
    xhr.setRequestHeader("Content-Type", "application/octet-stream");
    xhr.setRequestHeader("X-Auth-Token", service.token.token);


    xhr.addEventListener("readystatechange", function (e) {
      if (this.readyState === 4 && this.status >= 200 && this.status < 300) {
        transferComplete(e)
      }
      if (this.readyState === 4 && this.status === 0) {
        transferFailed(e)
      }
      if (this.status >= 400) {
        transferFailed(e)
      }

    });


    xhr.send(file);
}

Here is the exact curl command, formatted for readability:

curl 'https://my-website.com/54321/my-filename.jpg' 
  -X PUT   
  -H 'Pragma: no-cache' 
  -H 'Origin: https://my-website.com' 
  -H 'Accept-Encoding: gzip, deflate, sdch, br' 
  -H 'Accept-Language: en-US,en;q=0.8' 
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36' 
  -H 'Content-Type: application/octet-stream' 
  -H 'Accept: */*' 
  -H 'Cache-Control: no-cache' 
  -H 'X-Auth-Token: fsadgsdgs' 
  -H 'Referer: https://some-allowed-origin-referrer.com/' 
  -H 'Connection: keep-alive' 
  -H 'Content-Length: 86815' 
  -F "data=@/Users/satish/abc.jpg" --compressed --insecure 

//Headers stripped except for token

  curl 'https://my-website.com/54321/my-filename.jpg' 
    -X PUT  
    -H 'X-Auth-Token: fsadsdsdaf' 
    -F "data=@/Users/satish/abc.jpg" 
    --compressed --insecure 

--- Update 21-02-2017 ---

To rule out any API end point specific behaviour, I wrote a crude PHP script to test this observation and it is still true. Below is the php script I tried uploading to.

<?php
/**
 * If I comment out the below lines, then curl is failing immediately.
 * But XHR doesn't
 **/

// http_response_code(400);
// return;

/* PUT data comes in on the stdin stream */
$putdata = fopen( "php://input", "r" );

/* Open a file for writing */
$file = fopen( "/Users/satish/Work/Development/htdocs/put-test/wow.jpg", "w" );

/* Read the data 1 KB at a time
   and write to the file */
while ( $data = fread( $putdata, 1024 ) ) {
    fwrite( $file, $data );
}

/* Close the streams */
fclose( $file );
fclose( $putdata );
?>
Satish Gandham
  • 401
  • 4
  • 12
  • Can you show the CURL command you're using? – Pekka Feb 20 '17 at 08:30
  • Updated the question with CURL command. – Satish Gandham Feb 20 '17 at 10:56
  • I edited to make it more readable. Hmm, curious. Interested to learn why this is. Don't know enough about PUT to be able to tell myself. If there's no response in 48 hours, ping me here in a comment and I'll put a bounty on the question. (Those need 48 hours' wait) – Pekka Feb 20 '17 at 11:08
  • Have you tried adding all the headers the curl command is using? Namely the `Accept` one? Totally shooting in the dark, but who knows.... – Pekka Feb 20 '17 at 11:09
  • Thanks for editing and making the title more clear. The first curl command is generated from the actual XHR request. In chrome developer tools, you have the option to copy the request as curl from the network tab. I just added this `-F "data=@/Users/satish/abc.jpg"` to point it to the file. In the second curl, I removed unnecessary headers. – Satish Gandham Feb 20 '17 at 11:21
  • Ah, ok. If it works exactly the same with those headers removed, it might be worth removing the original curl command for the sake of brevity! – Pekka Feb 20 '17 at 11:54
  • 2
    Done. --------- – Pekka Feb 22 '17 at 09:03
  • 1
    I just tried this at my end (with my node.js based server) and both XHR and curl give similar behavior. Even curl takes time to throw error. curl or XHR both throw errors the moment they receive an error 40x from the server. I don't think there is anything wrong with XHR. If you are seeing this behavior, please check why your server sends an error late when XHR is used. If you are confident that your server is sending an error and XHR is reporting late, please add a tcpdump so that we can see the same in wireshark. – manishg Feb 23 '17 at 02:36
  • @manishg, See the php code I included. Even if I send an error immediately, XHR still uploads the whole file. Where as curl gets rejection immediately. Can you please share your node.js code? – Satish Gandham Feb 23 '17 at 10:08
  • I have pasted my server side code here (https://gist.github.com/manishganvir/c9be683b4ac0c48ca4f0926a93d32643). In case of XHR, can you check when the response is received from server in chrome dev tools? – manishg Feb 23 '17 at 14:43
  • You could use `ServiceWorker` to intercept request, try authenticating at server, if successful, proceed with request to server with file set at `Request` `body`, else respond with error to browser from `ServiceWorker`. – guest271314 Feb 24 '17 at 04:18
  • What is `file.key`? Is `file` a `File` object? – guest271314 Feb 24 '17 at 04:31
  • 1
    What's the exact error code received by curl? – Knu Feb 26 '17 at 05:14
  • @guest271314 It's a tag i added to keep track of the progress of files being uploaded. – Satish Gandham Feb 26 '17 at 08:27
  • @SatishGandham Where do you assign `"key"` property to `File` object? – guest271314 Feb 26 '17 at 17:54
  • @SatishGandham The issue appears to be that you `.send(file)` before authentication occurs? See [Basic Authentication With XMLHTTPRequest](http://stackoverflow.com/questions/1652178/basic-authentication-with-xmlhttprequest), _"Credentialed Requests By default, “credentials” such as Cookies and HTTP Auth information are not sent in cross-site requests using XMLHttpRequest. In order to send them, you have to set the withCredentials property of the XMLHttpRequest object. "_ [cross-site xmlhttprequest with CORS](https://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/) – guest271314 Feb 26 '17 at 18:23
  • You could make two requests. First request authenticates without sending `file`, if authentication is successful, call `upload()` – guest271314 Feb 26 '17 at 18:31
  • Brian's answer seems to solve the mystery. – Pekka Feb 26 '17 at 19:35
  • @Pekka웃, Yes :D Will make a separate request (Implement the curl probe model, or ask for a separate api from the service ) to see if the use has permissions to upload files and skip the upload process all together if he is not authorised. – Satish Gandham Feb 27 '17 at 20:15
  • @guest271314, All the required flags are set on the request url. Without probe feature, curl would also upload the whole file. I will have to implement the probe like functionality for curl as well. – Satish Gandham Feb 27 '17 at 20:18
  • Cool, and interesting solution this question had. Awarding Brian the bounty now. – Pekka Feb 27 '17 at 20:49

2 Answers2

5

When curl has a PUT request with authentication required, it first sends a “probe” with no content to allow the server the opportunity to refuse the connection before any data gets sent.

This doesn’t seem to be documented anywhere other than a comment in the code in Curl_http() in lib/http.c.

(Note that in versions of curl prior to 7.53.0 (2017‑02‑22, i.e. newer than the question), there was a bug whereby the user-supplied Content‑Length header (or no header) was sent instead of Content‑Length: 0.)

XHR implements no such probe and simply sends all the content with the initial request.

Brian Nixon
  • 9,398
  • 1
  • 18
  • 24
  • 1
    Probably worth noting that per https://github.com/curl/curl/blob/1f8023ceb5dc6b142c51fe161e0574b4d7f14b5e/lib/http.c#L1865 curl does this zero-length “probe” for POST requests too. – sideshowbarker Feb 26 '17 at 21:49
  • Thanks a lot @Brian, this has been bothering me for the past year. – Satish Gandham Feb 27 '17 at 20:20
2

Try adding -H "Expect:" to your curl command.

I might be wrong but here's my hunch:

  • XHR: Expect is a forbidden header name
  • curl: adds Expect: 100-continue by default
Knu
  • 14,806
  • 5
  • 56
  • 89