Method for streaming data from browser to server via HTTP
Asked Answered
A

6

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.

Arnulfoarny answered 9/3, 2016 at 18:19 Comment(17)
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?Shrill
@Shrill 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?Arnulfoarny
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?Shrill
Question may have already answer at: #6558629Amalita
@Shrill 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.Arnulfoarny
@Amalita 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.Arnulfoarny
@Arnulfoarny How is data created?Shrill
@Shrill 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.Arnulfoarny
@Arnulfoarny Yes, attempting to determine when, how frequently, data should be sent? Does generateSomeBinaryData() return UInt8Array?Shrill
@Shrill Yes, exactly. Let's say, 2 or 3 chunks per second are available, 8-16KB each.Arnulfoarny
@Arnulfoarny 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.Coomer
@Coomer Unfortunately, not quite that flexible. :-) I do need this to run in a normal browser session.Arnulfoarny
@Arnulfoarny 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.Coomer
@Coomer 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!Arnulfoarny
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.Dariadarian
@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.Arnulfoarny
For anyone with a use case to add regarding this feature, please check here: groups.google.com/a/chromium.org/forum/#!topic/…Arnulfoarny
C
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"
}
Coomer answered 19/6, 2016 at 17:2 Comment(1)
that's save my day!Smilax
S
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

Scalage answered 27/6, 2016 at 14:7 Comment(4)
This doesn't actually work. Chrome can't send a ReadableStream. It converts it to string and sends [object Object].Arnulfoarny
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... https://mcmap.net/q/128732/-fetch-with-readablestream-as-request-bodyArnulfoarny
Tests on the feature can be run here w3c-test.org/fetch/api/basic/request-upload.any.html (still failing as of today)Winnipegosis
For anyone interested in the current discussion, check here: groups.google.com/a/chromium.org/forum/#!topic/…Arnulfoarny
S
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

Shrill answered 19/6, 2016 at 1:0 Comment(5)
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?Budbudapest
@Budbudapest "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 aloneShrill
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".Budbudapest
@Budbudapest 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.Shrill
@Arnulfoarny See also Stream Consumers, Stream ProducersShrill
S
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
});
Shrill answered 16/6, 2016 at 4:43 Comment(13)
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.Arnulfoarny
@Arnulfoarny Are you trying to create Writable Streams? See also Creating your own readable streamShrill
@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.Budbudapest
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.Arnulfoarny
@Shrill 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.Arnulfoarny
@Arnulfoarny fetch, ReadableStream and ReadableByteStream are available at chromium 50. Should be possible, see github.com/w3c/web-platform-tests/blob/master/fetch/api/…Shrill
@Shrill I'm confused now. I thought @Arnulfoarny wanted to stream data to a server, so how would fetch and ReadableXXX help?Budbudapest
@Budbudapest Believe it should be possible to send stream as Request Body POST using fetch or ServiceWorker; see chromestatus.com/feature/5804334163951616, bugs.chromium.org/p/chromium/issues/detail?id=393911, https://mcmap.net/q/49469/-fetch-post-json-data, groups.google.com/a/chromium.org/forum/#!topic/blink-dev/…Shrill
@Budbudapest See also streams.spec.whatwg.org/branch-snapshots/deploy-issues , chromium.googlesource.com/chromium/blink/+/master/Source/core/…Shrill
@Shrill 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.Budbudapest
@Arnulfoarny When should connection to server be closed? That is, how to determine that all data sent to remote sink is complete?Shrill
@Shrill 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.Arnulfoarny
@Budbudapest "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 ReadableStreamShrill
S
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

Seafood answered 17/11, 2021 at 21:45 Comment(2)
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.Arnulfoarny
Thanks @brad, that is interesting to know. I guess there is some resistance to the chunked data encoding used in HTTP/1.1.Seafood
C
-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.

Clarabelle answered 16/6, 2016 at 5:5 Comment(7)
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.Arnulfoarny
Can't you generate a PUT request to the server with XMLHttpRequest ?Clarabelle
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?Clarabelle
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.Arnulfoarny
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.Clarabelle
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.Arnulfoarny
@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.Normanormal

© 2022 - 2024 — McMap. All rights reserved.