-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stream HTTP responses to WebView2 #3519
Comments
Hi @NickDarvey, Thanks for your advice! To help better understand your need, could you describe the specific usage scenario in detail? |
@plantree, I'm building a hybrid app where I have some web components (JS) hosted in a native (WinUI3) application. I want my web components to be able to use services offered by my application so that, for example:
Both examples would benefit from being able to intercept a HTTP request and stream responses back to the client. For (1), the web component could subscribe to the lifecycle events with a fetch to For (2), the web component could request the document with a fetch to The alternative is to correlate successive web messages ( This functionality is available on other platforms. For example, on iOS and macOS with WKWebView:
https://developer.apple.com/documentation/webkit/wkurlschemetask |
Hi @NickDarvey, Thanks for your information and professional analysis! If it's possible, could you provide a simple |
@plantree, I think the code I provided in my original post is a simple example of how it might work. |
Is this item saying sse is not currently supported by webview2? |
@PylotLight, SSE is not supported if you’re intercepting requests with WebView2’s WebResourceRequested filters, but it works fine otherwise. |
Hi Nick - I will track this feature request in our internal team backlog. In the meantime can you help me understand if you currently have a workaround ( my assumption is no? ) |
I wrapped Javascript part: (function () {
'use strict';
(function () {
if (!window.onKAppEventStream) {
const eventRegisters = {};
const onKAppEventStream = (data) => {
let { uuid, final, content } = data;
if (!eventRegisters[uuid]) {
eventRegisters[uuid] = {
instance: null,
pending: [],
};
}
if (final) {
content = null;
} else {
if (typeof content === 'string') {
content = new TextEncoder().encode(content);
}
}
eventRegisters[uuid].pending.push({ final, content });
if (eventRegisters[uuid].instance) {
eventRegisters[uuid].instance.notifyMessage();
}
};
Object.defineProperty(window, 'onKAppEventStream', {
value: onKAppEventStream,
configurable: true,
writable: true,
enumerable: false,
});
class KAppFetchEventStream {
constructor(eventStreamId) {
this.eventStreamId = eventStreamId;
this.finished = false;
this.finishResolver = new Promise(resolve => this.runFinishResolve = resolve);
}
cancel() {
return this.finishResolver;
}
notifyMessage() {
if (this.messageNotifier) {
const noti = this.messageNotifier;
this.messageNotifier = null;
noti();
}
}
onFinish() {
if (!this.finished) {
this.finished = true;
this.runFinishResolve();
delete eventRegisters[this.eventStreamId];
}
}
triggerFinal() {
if (!this.finished) {
setTimeout(() => this.onFinish(), 100);
}
}
async read() {
if (!eventRegisters[this.eventStreamId]) {
eventRegisters[this.eventStreamId] = {
instance: this,
pending: [],
};
} else {
eventRegisters[this.eventStreamId].instance = this;
}
if (eventRegisters[this.eventStreamId].pending.length == 0) {
while (this.messageNotifier) {
await new Promise(resolve => setTimeout(resolve, 100));
}
await new Promise(resolve => this.messageNotifier = resolve);
}
if (eventRegisters[this.eventStreamId].pending.length > 0) {
const { final, content } = eventRegisters[this.eventStreamId].pending.shift();
if (final) this.triggerFinal();
return { done: final, value: content };
}
}
releaseLock() {}
}
((fetch) => {
window.fetch = async function (uri, options, ...args) {
let r = await fetch.call(this, uri, options, ...args);
let eventStreamId = r.headers.get('kapp-event-stream');
if (eventStreamId) {
r.body.getReader = () => {
return new KAppFetchEventStream(eventStreamId);
}
}
return r;
};
})(fetch);
}
})();
})(); C# part: ...
// in handleWebResourceRequested:, when we get an event-stream response, just finish the response and use simulateStreamResponse to send parts to the website
if (contentType != null && contentType.Split(';')[0] == "text/event-stream")
{
var uuid = Guid.NewGuid().ToString();
_ = simulateStreamResponse(uuid, responseStream);
var emptyResponseStream = new InMemoryRandomAccessStream();
return contentWebView.CoreWebView2.Environment.CreateWebResourceResponse(
emptyResponseStream.AsStreamForRead(),
(int)response.StatusCode,
response.ReasonPhrase,
build_reponse_header(response, new_length: 0, additional: new string[] { $"kapp-event-stream: {uuid}" })
);
}
...
void sendStreamResponse(string uuid, bool final, string? content)
{
BeginInvoke(async () =>
{
var finalJS = final ? "true" : "false";
if (content == null)
{
await contentWebView.ExecuteScriptAsync($"(window.onKAppEventStream||console.log)({{uuid:'{uuid}',final:{finalJS},content:null}});");
}
else
{
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
var bytesString = "[" + String.Join(",", bytes) + "]";
var eval = $"(window.onKAppEventStream||console.log)({{uuid:'{uuid}',final:{finalJS},content:Uint8Array.from({bytesString})}});";
await contentWebView.ExecuteScriptAsync(eval);
}
});
}
async Task simulateStreamResponse(string uuid, Stream? stream)
{
try
{
if (stream == null)
{
return;
}
var reader = new StreamReader(stream);
var line = "";
while (!reader.EndOfStream && (line = await reader.ReadLineAsync()) != null)
{
sendStreamResponse(uuid, false, line + "\n");
await Task.Delay(10);
}
}
finally
{
sendStreamResponse(uuid, true, null);
}
} |
I tried to implement Response Streaming for Wails and for that purpose did a custom implementation of
So this would need something like ReadAsync support from IStreamAsync. Furthermore we would also need a way to find out if the request has been stopped by WebView2. For example let's say one does an SSE streaming and WebView2 does a reload of the document. In that case we would need a way to get informed that the request is getting stopped and the stop the SSE streaming process on the host side. |
@victorhuangwq @yildirimcagri-msft Any suggestions based on the comment from staffabi above? |
Any updates? We are in a similar situation, we want to stream HTTP responses to the client, tipically for download large files. It seems the response becomes available all-at-once to the client js. |
@vbryh-msft could you help us understand if this is something can already be done? And if this feature request is valid? |
skimmed through request and comments - does SharedBuffer API can be used there? |
SharedBuffer API does not help in this case, we would like to stream HTTP responses back. So the frontend code could use a simple fetch call or the EventSource API. |
+1 for this use case. I am looking at using something like this mostly for improved performance in getting data from the backend to the frontend, and while the SharedBuffer API could also provide this performance increase, it is a much lower-level API and requires lots of manual synchronization implementation which makes it an order of magnitude more complex. |
I want to stream HTTP responses to WebView2. I don't think this is possible right now, so this is a feature request.
For example, I want to intercept a request and return server-sent events which the client can process per event.
For example, I want to intercept a request and return a chunked response which the client can process incrementally.
Repro
(Or how it could work)
Host C# code
Client JavaScript code
Expected
I want the response to be streamed so the client can process each event as it's sent.
For example, running the JavaScript client code with the url
https://sse.dev/test
will behave like this.Actual
The response is buffered and becomes available to the client all-at-once.
System info
AB#47606624
The text was updated successfully, but these errors were encountered: