18

Are there any XHR-like browser APIs available for streaming binary to a server over HTTP?

I want to make an HTTP PUT request and create data programmatically, over time. I don't want to create all this data at once, since there could be gigs of it sitting in memory. Some psueudo-code to illustrate what I'm getting at:

var dataGenerator = new DataGenerator(); // Generates 8KB UInt8Array every second
var streamToWriteTo;
http.put('/example', function (requestStream) {
  streamToWriteTo = requestStream;
});

dataGenerator.on('data', function (chunk) {
  if (!streamToWriteTo) {
    return;
  }
  streamToWriteTo.write(chunk);
});

I currently have a web socket solution in place instead, but would prefer regular HTTP for better interop with some existing server-side code.

EDIT: I can use bleeding edge browser APIs. I was looking at the Fetch API, as it supports ArrayBuffers, DataViews, Files, and such for request bodies. If I could somehow fake out one of these objects so that I could use the Fetch API with dynamic data, that would work for me. I tried creating a Proxy object to see if any methods were called that I could monkey patch. Unfortunately, it seems that the browser (at least in Chrome) is doing the reading in native code and not in JS land. But, please correct me if I'm wrong on that.

Brad
  • 159,648
  • 54
  • 349
  • 530
  • Is expected result that at some moment in future all of the data sent to server would be retrievable at single URI? Should each body of data sent to server create a new and distinct URI containing data? Or, should each body of data sent to server overwrite previously sent data? – guest271314 Jun 16 '16 at 02:50
  • @guest271314 What happens server-side doesn't matter. But, I do need to stream data in a single HTTP request. That is, when the HTTP request is started, I don't have all of the data yet. The data is created dynamically and streamed on the fly. I need to PUT/POST data as it is created. Does that make sense? – Brad Jun 16 '16 at 04:02
  • `js` at Question does not achieve this? You could probably replace `setInterval` with an approach which sends data when created. Is total `Content-Length` of data available when process begins? – guest271314 Jun 16 '16 at 04:07
  • Question may have already answer at: http://stackoverflow.com/questions/6558129/process-a-continuous-stream-of-json – mico Jun 16 '16 at 04:09
  • @guest271314 No, the JS in the question is pseudo-code. `setInterval()` is there as a placeholder to whatever would send data. I'm asking for a way to do that. `Content-Length` isn't known, but I can fake it with a high value and clean that up on the server side, so if it's helpful to have a content length, then that's fine. – Brad Jun 16 '16 at 04:10
  • 1
    @mico The question you linked to is for handling response data, and isn't appropriate for streaming request data. Also, what I want to do is possible with web sockets, but I have an edge case I need to work around where web socket usage isn't available. Fortunately, I don't need all the capability of web sockets... I just need to send data to the server. it's just that the data isn't known yet until it's created. – Brad Jun 16 '16 at 04:11
  • @Brad How is data created? – guest271314 Jun 16 '16 at 04:12
  • @guest271314 Arbitrarily. Doesn't matter. Ideally, each chunk is a binary byte array (`UInt8Array`). Whichever way works. :-) I can finagle a string if I have to, but would much prefer to work with binary data. For the sake of the example, let's assume I call a function and get a binary array back. Say, 8KB of random or arbitrary data. – Brad Jun 16 '16 at 04:14
  • @Brad Yes, attempting to determine when, how frequently, data should be sent? Does `generateSomeBinaryData()` return `UInt8Array`? – guest271314 Jun 16 '16 at 04:16
  • @guest271314 Yes, exactly. Let's say, 2 or 3 chunks per second are available, 8-16KB each. – Brad Jun 16 '16 at 04:17
  • @Brad It sounds like you are very flexible with regards to the browser - e.g. dev releases and enabling experimental features seem okay with you. Would that flexibility extend to installing a Chrome App? Chrome Apps have access to the `chrome.sockets.tcp` API, with which you can build your own streaming upload HTTP client. – rhashimoto Jun 18 '16 at 22:32
  • @rhashimoto Unfortunately, not quite that flexible. :-) I do need this to run in a normal browser session. – Brad Jun 18 '16 at 22:33
  • @Brad Just to be completely sure, you do know that a Chrome App can be a background service for your regular browser page, right? You could use it simply to implement an additional streaming API to the browser. – rhashimoto Jun 18 '16 at 22:59
  • @rhashimoto Thanks for the suggestion. At the moment, I have an installed background app (outside of Chrome, in Node.js) doing some protocol translation. I didn't realize Chrome Apps could be hooked into from the page. I will look into this option if I can't find a more direct route. Thanks! – Brad Jun 18 '16 at 23:02
  • It might sound weird, but I would use WebRTC. You can use it to create a data channel between two peers, where one of them is always your server. – Andrey Kiselev Jun 19 '16 at 21:05
  • 1
    @AndreyKiselev Not weird at all. But, for the server side I already have web sockets which is a much simpler solution than WebRTC. I'd like to use HTTP, straight up. – Brad Jun 19 '16 at 21:06
  • For anyone with a use case to add regarding this feature, please check here: https://groups.google.com/a/chromium.org/forum/#!topic/blink-network-dev/bsVgOxNCzFc – Brad Sep 09 '19 at 02:12

6 Answers6

7

I don't know how to do this with pure HTML5 APIs, but one possible workaround is to use a Chrome App as a background service to provide additional features to a web page. If you're already willing to use development browsers and enable experimental features, this seems like just an incremental step further than that.

Chrome Apps can call the chrome.sockets.tcp API, on which you can implement any protocol you want, including HTTP and HTTPS. This would provide the flexibility to implement streaming.

A regular web page can exchange messages with an App using the chrome.runtime API, as long as the App declares this usage. This would allow your web page to make asynchronous calls to your App.

I wrote this simple App as a proof of concept:

manifest.json

{
  "manifest_version" : 2,

  "name" : "Streaming Upload Test",
  "version" : "0.1",

  "app": {
    "background": {
      "scripts": ["background.js"]
    }
  },

  "externally_connectable": {
    "matches": ["*://localhost/*"]
  },

  "sockets": {
    "tcp": {
      "connect": "*:*"
    }
  },

  "permissions": [
  ]
}

background.js

var mapSocketToPort = {};

chrome.sockets.tcp.onReceive.addListener(function(info) {
  var port = mapSocketToPort[info.socketId];
  port.postMessage(new TextDecoder('utf-8').decode(info.data));
});

chrome.sockets.tcp.onReceiveError.addListener(function(info) {
  chrome.sockets.tcp.close(info.socketId);
  var port = mapSocketToPort[info.socketId];
  port.postMessage();
  port.disconnect();
  delete mapSocketToPort[info.socketId];
});

// Promisify socket API for easier operation sequencing.
// TODO: Check for error and reject.
function socketCreate() {
  return new Promise(function(resolve, reject) {
    chrome.sockets.tcp.create({ persistent: true }, resolve);
  });
}

function socketConnect(s, host, port) {
  return new Promise(function(resolve, reject) {
    chrome.sockets.tcp.connect(s, host, port, resolve);
  });
}

function socketSend(s, data) {
  return new Promise(function(resolve, reject) {
    chrome.sockets.tcp.send(s, data, resolve);
  });
}

chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    if (!port.state) {
      port.state = msg;

      port.chain = socketCreate().then(function(info) {
        port.socket = info.socketId;
        mapSocketToPort[port.socket] = port;
        return socketConnect(port.socket, 'httpbin.org', 80);
      }).then(function() {
        // TODO: Layer TLS if needed.
      }).then(function() {
        // TODO: Build headers from the request.
        // TODO: Use Transfer-Encoding: chunked.
        var headers =
            'PUT /put HTTP/1.0\r\n' +
            'Host: httpbin.org\r\n' +
            'Content-Length: 17\r\n' +
            '\r\n';
        return socketSend(port.socket, new TextEncoder('utf-8').encode(headers).buffer);
      });
    }
    else {
      if (msg) {
        port.chain = port.chain.then(function() {
          // TODO: Use chunked encoding.
          return socketSend(port.socket, new TextEncoder('utf-8').encode(msg).buffer);
        });
      }
    }
  });
});

This App does not have a user interface. It listens for connections and makes a hard-coded PUT request to http://httpbin.org/put (httpbin is a useful test site but note it does not support chunked encoding). The PUT data (currently hard-coded to exactly 17 octets) is streamed in from the client (using as few or as many messages as desired) and sent to the server. The response from the server is streamed back to the client.

This is just a proof of concept. A real App should probably:

  • Connect to any host and port.
  • Use Transfer-Encoding: chunked.
  • Signal the end of streaming data.
  • Handle socket errors.
  • Support TLS (e.g. with Forge)

Here is a sample web page that performs a streaming upload (of 17 octets) using the App as a service (note that you will have to configure your own App id):

<pre id="result"></pre>
<script>
 var MY_CHROME_APP_ID = 'omlafihmmjpklmnlcfkghehxcomggohk';

 function streamingUpload(url, options) {
   // Open a connection to the Chrome App. The argument must be the 
   var port = chrome.runtime.connect(MY_CHROME_APP_ID);

   port.onMessage.addListener(function(msg) {
     if (msg)
       document.getElementById("result").textContent += msg;
     else
       port.disconnect();
   });

   // Send arguments (must be JSON-serializable).
   port.postMessage({
     url: url,
     options: options
   });

   // Return a function to call with body data.
   return function(data) {
     port.postMessage(data);
   };
 }

 // Start an upload.
 var f = streamingUpload('https://httpbin.org/put', { method: 'PUT' });

 // Stream data a character at a time.
 'how now brown cow'.split('').forEach(f);
</script>

When I load this web page in a Chrome browser with the App installed, httpbin returns:

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 19 Jun 2016 16:54:23 GMT
Content-Type: application/json
Content-Length: 240
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "args": {}, 
  "data": "how now brown cow", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Content-Length": "17", 
    "Host": "httpbin.org"
  }, 
  "json": null, 
  "origin": "[redacted]", 
  "url": "http://httpbin.org/put"
}
rhashimoto
  • 15,650
  • 2
  • 52
  • 80
7



I'm currently searching for exactly the same thing (upstreaming via Ajax). What I currently found, looks as if we are searching at the bleeding edge of browser's feature design ;-)

XMLHttpRequest definition tells in step 4 bodyinit that the content extraction of this is (or can be) a readablestream.

I'm still searching (as a non-webdeveloper) for information of how to create such a thing and to feed data into the "other end" of that "readablestream" (which namely should be a "writablestream", but I yet did not find that).

Maybe you are better in searching and can post here if you found a method to implement these design plans.

^5
sven

Synopsis
  • 191
  • 1
  • 5
  • 2
    This doesn't actually work. Chrome can't send a ReadableStream. It converts it to string and sends `[object Object]`. – Brad Aug 24 '16 at 00:35
  • 1
    An update... Chrome is no longer casting ReadableStream as a string, but it's not sending any data either. I've created a spin-off question for this specific method... http://stackoverflow.com/questions/40939857/fetch-with-readablestream – Brad Dec 02 '16 at 19:52
  • 1
    Tests on the feature can be run here http://w3c-test.org/fetch/api/basic/request-upload.any.html (still failing as of today) – Pierre Mar 07 '19 at 17:53
  • For anyone interested in the current discussion, check here: https://groups.google.com/a/chromium.org/forum/#!topic/blink-network-dev/bsVgOxNCzFc – Brad Sep 09 '19 at 02:12
1

An approach utilizing ReadableStream to stream arbitrary data; RTCDataChannel to send and, or, receive arbitrary data in form of Uint8Array; TextEncoder to create 8000 bytes of random data stored in a Uint8Array, TextDecoder to decode Uint8Array returned by RTCDataChannel to string for presentation, note could alternatively use FileReader .readAsArrayBuffer and .readAsText here.

The markup and script code were modified from examples at MDN - WebRTC: Simple RTCDataChannel sample, including adapter.js which contains RTCPeerConnection helpers; Creating your own readable stream.

Note also, example stream is cancelled when total bytes transferred reaches 8000 * 8 : 64000

(function init() {
  var interval, reader, stream, curr, len = 0,
    totalBytes = 8000 * 8,
    data = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
    randomData = function randomData() {
      var encoder = new TextEncoder();
      var currentStream = "";
      for (var i = 0; i < 8000; i++) {
        currentStream += data[Math.floor(Math.random() * data.length)]
      }
      return encoder.encode(currentStream)
    },
    // optionally reconnect to stream if cancelled
    reconnect = function reconnect() {
      connectButton.disabled = false;
      startup()
    };

  // Define "global" variables

  var connectButton = null;
  var disconnectButton = null;
  var messageInputBox = null;
  var receiveBox = null;

  var localConnection = null; // RTCPeerConnection for our "local" connection
  // adjust this to remote address; or use `ServiceWorker` `onfetch`; other
  var remoteConnection = null; // RTCPeerConnection for the "remote"

  var sendChannel = null; // RTCDataChannel for the local (sender)
  var receiveChannel = null; // RTCDataChannel for the remote (receiver)

  // Functions

  // Set things up, connect event listeners, etc.

  function startup() {
    connectButton = document.getElementById("connectButton");
    disconnectButton = document.getElementById("disconnectButton");
    messageInputBox = document.getElementById("message");
    receiveBox = document.getElementById("receivebox");

    // Set event listeners for user interface widgets

    connectButton.addEventListener("click", connectPeers, false);
    disconnectButton.addEventListener("click", disconnectPeers, false);
  }

  // Connect the two peers. Normally you look for and connect to a remote
  // machine here, but we"re just connecting two local objects, so we can
  // bypass that step.

  function connectPeers() {
    // Create the local connection and its event listeners
    if (len < totalBytes) {
      localConnection = new RTCPeerConnection();

      // Create the data channel and establish its event listeners
      sendChannel = localConnection.createDataChannel("sendChannel");
      sendChannel.onopen = handleSendChannelStatusChange;
      sendChannel.onclose = handleSendChannelStatusChange;

      // Create the remote connection and its event listeners

      remoteConnection = new RTCPeerConnection();
      remoteConnection.ondatachannel = receiveChannelCallback;

      // Set up the ICE candidates for the two peers

      localConnection.onicecandidate = e => 
        !e.candidate || remoteConnection.addIceCandidate(e.candidate)
      .catch(handleAddCandidateError);

      remoteConnection.onicecandidate = e => 
        !e.candidate || localConnection.addIceCandidate(e.candidate)
      .catch(handleAddCandidateError);

      // Now create an offer to connect; this starts the process

      localConnection.createOffer()
      .then(offer => localConnection.setLocalDescription(offer))
      .then(() => remoteConnection
                 .setRemoteDescription(localConnection.localDescription)
       )
      .then(() => remoteConnection.createAnswer())
      .then(answer => remoteConnection
                      .setLocalDescription(answer)
       )
      .then(() => localConnection
                 .setRemoteDescription(remoteConnection.localDescription)
      )
      // start streaming connection
      .then(sendMessage)
      .catch(handleCreateDescriptionError);
    } else {

      alert("total bytes streamed:" + len)
    }

  }

  // Handle errors attempting to create a description;
  // this can happen both when creating an offer and when
  // creating an answer. In this simple example, we handle
  // both the same way.

  function handleCreateDescriptionError(error) {
    console.log("Unable to create an offer: " + error.toString());
  }

  // Handle successful addition of the ICE candidate
  // on the "local" end of the connection.

  function handleLocalAddCandidateSuccess() {
    connectButton.disabled = true;
  }

  // Handle successful addition of the ICE candidate
  // on the "remote" end of the connection.

  function handleRemoteAddCandidateSuccess() {
    disconnectButton.disabled = false;
  }

  // Handle an error that occurs during addition of ICE candidate.

  function handleAddCandidateError() {
    console.log("Oh noes! addICECandidate failed!");
  }

  // Handles clicks on the "Send" button by transmitting
  // a message to the remote peer.

  function sendMessage() {

    stream = new ReadableStream({
      start(controller) {
          interval = setInterval(() => {
            if (sendChannel) {
              curr = randomData();
              len += curr.byteLength;
              // queue current stream
              controller.enqueue([curr, len, sendChannel.send(curr)]);

              if (len >= totalBytes) {
                controller.close();
                clearInterval(interval);
              }
            }
          }, 1000);
        },
        pull(controller) {
          // do stuff during stream
          // call `releaseLock()` if `diconnect` button clicked
          if (!sendChannel) reader.releaseLock();
        },
        cancel(reason) {
          clearInterval(interval);
          console.log(reason);
        }
    });

    reader = stream.getReader({
      mode: "byob"
    });

    reader.read().then(function process(result) {
        if (result.done && len >= totalBytes) {
          console.log("Stream done!");
          connectButton.disabled = false;
          if (len < totalBytes) reconnect();
          return;
        }

        if (!result.done && result.value) {
          var [currentStream, totalStreamLength] = [...result.value];
        }

        if (result.done && len < totalBytes) {
          throw new Error("stream cancelled")
        }

        console.log("currentStream:", currentStream
                   , "totalStremalength:", totalStreamLength
                   , "result:", result);
        return reader.read().then(process);
      })
      .catch(function(err) {
        console.log("catch stream cancellation:", err);
        if (len < totalBytes) reconnect()
      });

    reader.closed.then(function() {
      console.log("stream closed")
    })

  }

  // Handle status changes on the local end of the data
  // channel; this is the end doing the sending of data
  // in this example.

  function handleSendChannelStatusChange(event) {
    if (sendChannel) {
      var state = sendChannel.readyState;

      if (state === "open") {
        disconnectButton.disabled = false;
        connectButton.disabled = true;
      } else {
        connectButton.disabled = false;
        disconnectButton.disabled = true;
      }
    }
  }

  // Called when the connection opens and the data
  // channel is ready to be connected to the remote.

  function receiveChannelCallback(event) {
    receiveChannel = event.channel;
    receiveChannel.onmessage = handleReceiveMessage;
    receiveChannel.onopen = handleReceiveChannelStatusChange;
    receiveChannel.onclose = handleReceiveChannelStatusChange;
  }

  // Handle onmessage events for the receiving channel.
  // These are the data messages sent by the sending channel.

  function handleReceiveMessage(event) {
    var decoder = new TextDecoder();
    var data = decoder.decode(event.data);
    var el = document.createElement("p");
    var txtNode = document.createTextNode(data);

    el.appendChild(txtNode);
    receiveBox.appendChild(el);
  }

  // Handle status changes on the receiver"s channel.

  function handleReceiveChannelStatusChange(event) {
    if (receiveChannel) {
      console.log("Receive channel's status has changed to " +
        receiveChannel.readyState);
    }

    // Here you would do stuff that needs to be done
    // when the channel"s status changes.
  }

  // Close the connection, including data channels if they"re open.
  // Also update the UI to reflect the disconnected status.

  function disconnectPeers() {

    // Close the RTCDataChannels if they"re open.

    sendChannel.close();
    receiveChannel.close();

    // Close the RTCPeerConnections

    localConnection.close();
    remoteConnection.close();

    sendChannel = null;
    receiveChannel = null;
    localConnection = null;
    remoteConnection = null;

    // Update user interface elements


    disconnectButton.disabled = true;
    // cancel stream on `click` of `disconnect` button, 
    // pass `reason` for cancellation as parameter
    reader.cancel("stream cancelled");
  }

  // Set up an event listener which will run the startup
  // function once the page is done loading.

  window.addEventListener("load", startup, false);
})();

plnkr http://plnkr.co/edit/cln6uxgMZwE2EQCfNXFO?p=preview

Armen Michaeli
  • 8,625
  • 8
  • 58
  • 95
guest271314
  • 1
  • 15
  • 104
  • 177
  • Interesting. Note your answer is missing a critical step in that an experimental flag must be enabled in Chrome to use `ReadableStream`. Also, the *Disconnect* button throws an error: `Cannot read property 'cancel' of undefined`. However, the OP's problem requires HTTP (PUT/POST), yet your solution uses WebRTC. Were you unable to get `fetch` and `ReadableStream` to work as you [intended](http://stackoverflow.com/questions/35899536/method-for-streaming-data-from-browser-to-server-via-http/37903033#comment63221572_37850033)? – tony19 Jun 19 '16 at 04:33
  • @tony19 _"Note your answer is missing a critical step in that an experimental flag must be enabled in Chrome "_ Fair point. OP appears to be familiar with browser technology _"I can use bleeding edge browser APIs"_, though yes, tried with `--enable-experimental-web-platform-features` flag set at chromium. _"the OP's problem requires HTTP (PUT/POST),"_ Did not gather from Question, comments that only `PUT` , `POST` had to be used _"but I have an edge case I need to work around"_ . _"Cannot read property 'cancel' of undefined"_ Error does not occur here. Not certain possible using `fetch` alone – guest271314 Jun 19 '16 at 04:45
  • 1
    Really? **Title:** "to server via HTTP", **Body:** "to a server over HTTP", "I want to make an HTTP PUT request", "prefer regular HTTP for better interop". To me, that all implies `PUT` or `POST`. :-) But I guess he would consider WebRTC since he mentioned "bleeding edge APIs". – tony19 Jun 19 '16 at 05:03
  • @tony19 Another option could be to use `browserify` to convert `nodejs` equivalent `PUT`, `POST`, writable stream functionality and implementation to file usable in browser. Not certain how lengthy the file would be. – guest271314 Jun 19 '16 at 16:16
  • @Brad See also [Stream Consumers](https://www.w3.org/TR/streams-api/#consumers), [Stream Producers](https://www.w3.org/TR/streams-api/#producers) – guest271314 Jun 21 '16 at 01:27
0

You could use Promise , setTimeout, recursion. See also PUT vs POST in REST

var count = 0, total = 0, timer = null, d = 500, stop = false, p = void 0
, request = function request () {
              return new XMLHttpRequest()
            };
function sendData() {
  p = Promise.resolve(generateSomeBinaryData()).then(function(data) { 
    var currentRequest = request();
    currentRequest.open("POST", "http://example.com");
    currentRequest.onload = function () {
      ++count; // increment `count`
      total += data.byteLength; // increment total bytes posted to server
    }

    currentRequest.onloadend = function () {
      if (stop) { // stop recursion
        throw new Error("aborted") // `throw` error to `.catch()`
      } else {
        timer = setTimeout(sendData, d); // recursively call `sendData`
      }
    }
    currentRequest.send(data); // `data`: `Uint8Array`; `TypedArray`
    return currentRequest; // return `currentRequest` 
  });
  return p // return `Promise` : `p`
}

var curr = sendData();

curr.then(function(current) {
  console.log(current) // current post request
})
.catch(function(err) {
  console.log(e) // handle aborted `request`; errors
});
Community
  • 1
  • 1
guest271314
  • 1
  • 15
  • 104
  • 177
  • Thank you for taking a crack at this. However, your code makes a new HTTP request for every chunk. I need to keep everything in a single HTTP request. I made a new example in the question so we don't get bogged down with how the data is generated. The question here is how to keep putting data in the request body of a single request while the request is in-flight. I'm wondering if there is a way to implement an interface like ArrayBufferView does so that XHR can use it in this way. Or, even modify a Blob somehow. – Brad Jun 16 '16 at 05:21
  • @Brad Are you trying to create [Writable Streams](https://streams.spec.whatwg.org/#ws-model)? See also [Creating your own readable stream](https://jakearchibald.com/2016/streams-ftw/#creating-your-own-readable-stream) – guest271314 Jun 16 '16 at 05:50
  • 1
    @guest271314, I don't believe `WritableStream` is available in any browser yet. `ReadableStream` only recently became available in Chrome and Opera. But Node does implement a [`WritableStream`](https://nodejs.org/api/stream.html#stream_class_stream_writable). – tony19 Jun 16 '16 at 10:01
  • Yes, a WritableStream would be great, but it can be anything as long as I can make an HTTP request with it and stream data to the server. And, I can use bleeding edge browser functionality. – Brad Jun 16 '16 at 16:11
  • @guest271314 I know how to use streams in Node.js. That's irrelevant to my question. I need to stream data to an HTTP request body. That's all. It doesn't necessarily have to be a stream... it can be anything. I can build my own abstraction. – Brad Jun 17 '16 at 05:19
  • @Brad `fetch`, `ReadableStream` and `ReadableByteStream` are available at chromium 50. Should be possible, see https://github.com/w3c/web-platform-tests/blob/master/fetch/api/response/response-stream-disturbed-1.html – guest271314 Jun 17 '16 at 05:26
  • 1
    @guest271314 I'm confused now. I thought @Brad wanted to stream data **to** a server, so how would `fetch` and `ReadableXXX` help? – tony19 Jun 17 '16 at 10:32
  • @tony19 Believe it should be possible to send stream as `Request` `Body` `POST` using `fetch` or `ServiceWorker`; see https://www.chromestatus.com/feature/5804334163951616, https://bugs.chromium.org/p/chromium/issues/detail?id=393911, http://stackoverflow.com/questions/29775797/fetch-post-json-data, https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/35_QSL1ABTY – guest271314 Jun 17 '16 at 13:56
  • @tony19 See also https://streams.spec.whatwg.org/branch-snapshots/deploy-issues/ , https://chromium.googlesource.com/chromium/blink/+/master/Source/core/streams/ReadableStream.cpp – guest271314 Jun 17 '16 at 14:37
  • 3
    @guest271314 Hmm. The links you provided all seem to indicate that the response is a `ReadableStream` and doesn't mention anything about the request body. Was there a specific section I needed to see? In any case, I'm looking forward to your implementation of this. – tony19 Jun 17 '16 at 15:00
  • @Brad When should connection to server be closed? That is, how to determine that all data sent to remote sink is complete? – guest271314 Jun 18 '16 at 19:05
  • @guest271314 Ideally, there'd be a `EOF` event of some kind, but it doesn't matter. I can close the connection server-side if I have to. Or, the request could end on its own once the amount of data in the `Content-Length` has been reached. – Brad Jun 18 '16 at 20:56
  • @tony19 _"The links you provided all seem to indicate that the response is a ReadableStream and doesn't mention anything about the request body."_ You are correct. `Response.body` implements `ReadableStream` – guest271314 Jul 11 '16 at 04:20
0

I think the short answer is no. As of writing this response (November 2021) this is not available in any of the major browsers.

The long answer is:

I think you are looking in the right place with the Fetch API. ReadableStream is currently a valid type for the body property of the Request constructor:
https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters

However, sadly if you look at the browser support matrix:
https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#browser_compatibility
you can see that "Send ReadableStream in request body" is still No for all the major browsers. Though it is currently available in experimental mode in some browsers (including Chrome).

There is a nice tutorial on how to do it in experimental mode here:
https://web.dev/fetch-upload-streaming/

Looking at the dates of the posts and the work done on this feature, I think it looks pretty clear that this technology is stagnating and we probably won't see it anytime soon. Consequently, WebSockets are probably still sadly one of our few good options (for unbounded stream transfers):
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

Chris Malek
  • 103
  • 9
  • 1
    Hey, streaming request bodies is actually in a Chrome origin trial right now!! Unfortunately, the authors decided to forbid us from using it on HTTP/1.1. HTTP/2 only. – Brad Nov 18 '21 at 02:54
  • Thanks @brad, that is interesting to know. I guess there is some resistance to the chunked data encoding used in HTTP/1.1. – Chris Malek Nov 20 '21 at 13:25
-3

Server-Sent Events and WebSockets are the preferred methods but in your case you are wanting to create a Representational state transfer, REST, API and use Long Polling. See How do I implement basic “Long Polling”?

Long polling process is handled both on the client side and server side. The server script and http server must be configured to support long polling.

Other than long polling, short polling (XHR/AJAX) requires the browser to poll the server.

Community
  • 1
  • 1
Ronnie Royston
  • 16,778
  • 6
  • 77
  • 91
  • Long polling is only useful for getting the response data back from the server. I need to stream request data to the server. I'm using web socket, but have a weird edge case I need to work around. Since I don't need all the capabilities of web socket, an HTTP PUT request is appropriate. The trick is, getting the browser to let me do it. – Brad Jun 16 '16 at 05:09
  • Can't you [generate a PUT request to the server with XMLHttpRequest](http://www.coderanch.com/t/475005/HTML-CSS-JavaScript/Generate-PUT-request-server-XMLHttpRequest) ? – Ronnie Royston Jun 16 '16 at 20:05
  • Long polling (or Comet) keeps an HTTP connection open with the server. It is useful for instant sending as the TCP connection is already up. Isn't that what you want to do? – Ronnie Royston Jun 16 '16 at 20:14
  • No, that's not what I want to do. I want to stream a request body. `XHR.send()` requires all of the request body to be present at the time it is called. Long polling is only useful for dealing with latent response data, which is completely unrelated. – Brad Jun 16 '16 at 21:11
  • XHR.send() returns as soon as the request is sent. Blob.slice method to split the Blob up into multiple chunks, and send each one as a separate request. [MDN Sending and Receiving Binary Data](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data). – Ronnie Royston Jun 16 '16 at 23:35
  • Sorry, but that doesn't solve my problem. I need to send data in the same request. I can't make a new request every time I have a new chunk of data. – Brad Jun 16 '16 at 23:38
  • 1
    @RonRoyston Besides, he doesn't have a blob to send immediately -- what you call blob is a stream here, the stream has indefinite size, it's a queue of data, and that's why chunked transfer encoding is desired here. – Armen Michaeli May 10 '17 at 14:21