5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 22:13:36 +08:00

[darwin] Add support for Request/Response streaming (#2219)

This commit is contained in:
stffabi 2022-12-20 13:44:33 +01:00 committed by GitHub
parent e2f1429c67
commit a312c0ffcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 352 additions and 127 deletions

View File

@ -48,7 +48,10 @@ const bool IsFullScreen(void *ctx);
const bool IsMinimised(void *ctx); const bool IsMinimised(void *ctx);
const bool IsMaximised(void *ctx); const bool IsMaximised(void *ctx);
void ProcessURLResponse(void *inctx, unsigned long long requestId, int statusCode, void *headersString, int headersStringLength, void* data, int datalength); void ProcessURLDidReceiveResponse(void *inctx, unsigned long long requestId, int statusCode, void *headersString, int headersStringLength);
bool ProcessURLDidReceiveData(void *inctx, unsigned long long requestId, void* data, int datalength);
void ProcessURLDidFinish(void *inctx, unsigned long long requestId);
int ProcessURLRequestReadBodyStream(void *inctx, unsigned long long requestId, void *buf, int bufLen);
/* Dialogs */ /* Dialogs */

View File

@ -52,12 +52,33 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
return result; return result;
} }
void ProcessURLResponse(void *inctx, unsigned long long requestId, int statusCode, void *headersString, int headersStringLength, void* data, int datalength) { void ProcessURLDidReceiveResponse(void *inctx, unsigned long long requestId, int statusCode, void *headersString, int headersStringLength) {
WailsContext *ctx = (__bridge WailsContext*) inctx; WailsContext *ctx = (__bridge WailsContext*) inctx;
@autoreleasepool { @autoreleasepool {
NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength]; NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength];
[ctx processURLDidReceiveResponse:requestId :statusCode :nsHeadersJSON];
}
}
bool ProcessURLDidReceiveData(void *inctx, unsigned long long requestId, void* data, int datalength) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
@autoreleasepool {
NSData *nsdata = [NSData dataWithBytes:data length:datalength]; NSData *nsdata = [NSData dataWithBytes:data length:datalength];
[ctx processURLResponse:requestId :statusCode :nsHeadersJSON :nsdata]; return [ctx processURLDidReceiveData:requestId :nsdata];
}
}
void ProcessURLDidFinish(void *inctx, unsigned long long requestId) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
@autoreleasepool {
[ctx processURLDidFinish:requestId];
}
}
int ProcessURLRequestReadBodyStream(void *inctx, unsigned long long requestId, void *buf, int bufLen) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
@autoreleasepool {
return [ctx processURLRequestReadBodyStream:requestId :buf :bufLen];
} }
} }

View File

@ -89,7 +89,10 @@
- (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters; - (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters;
- (void) loadRequest:(NSString*)url; - (void) loadRequest:(NSString*)url;
- (void) processURLResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersString :(NSData*)data; - (void) processURLDidReceiveResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON;
- (bool) processURLDidReceiveData:(unsigned long long)requestId :(NSData *)data;
- (void) processURLDidFinish:(unsigned long long)requestId;
- (int) processURLRequestReadBodyStream:(unsigned long long)requestId :(void *)buf :(int)bufLen;
- (void) ExecJS:(NSString*)script; - (void) ExecJS:(NSString*)script;
- (NSScreen*) getCurrentScreen; - (NSScreen*) getCurrentScreen;

View File

@ -15,6 +15,8 @@
#import "message.h" #import "message.h"
#import "Role.h" #import "Role.h"
typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
@implementation WailsWindow @implementation WailsWindow
- (BOOL)canBecomeKeyWindow - (BOOL)canBecomeKeyWindow
@ -107,7 +109,6 @@
[self.mouseEvent release]; [self.mouseEvent release];
[self.userContentController release]; [self.userContentController release];
[self.urlRequests release]; [self.urlRequests release];
[self.urlRequestsLock release];
[self.applicationMenu release]; [self.applicationMenu release];
[super dealloc]; [super dealloc];
} }
@ -138,7 +139,6 @@
- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight { - (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight {
self.urlRequestsId = 0; self.urlRequestsId = 0;
self.urlRequestsLock = [NSLock new];
self.urlRequests = [NSMutableDictionary new]; self.urlRequests = [NSMutableDictionary new];
NSWindowStyleMask styleMask = 0; NSWindowStyleMask styleMask = 0;
@ -427,37 +427,57 @@
}]; }];
} }
- (void) processURLResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON :(NSData *)data { - (void) processURLDidReceiveResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON {
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId]; [self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> urlSchemeTask) {
[self.urlRequestsLock lock];
id<WKURLSchemeTask> urlSchemeTask = self.urlRequests[key];
[self.urlRequestsLock unlock];
@try {
if (urlSchemeTask == nil) {
return;
}
NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData: headersJSON options: NSJSONReadingMutableContainers error: nil]; NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData: headersJSON options: NSJSONReadingMutableContainers error: nil];
NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease]; NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease];
@try { [urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveResponse:response]; }];
[urlSchemeTask didReceiveData:data]; }
[urlSchemeTask didFinish];
} @catch (NSException *exception) { - (bool) processURLDidReceiveData:(unsigned long long)requestId :(NSData *)data {
// This is very bad to detect a stopped schemeTask this should be implemented in a better way return [self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> urlSchemeTask) {
// See todo in stopURLSchemeTask... [urlSchemeTask didReceiveData:data];
if (![exception.reason isEqualToString: @"This task has already been stopped"]) { }];
@throw exception; }
- (void) processURLDidFinish:(unsigned long long)requestId {
[self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> urlSchemeTask) {
[urlSchemeTask didFinish];
}];
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
[self removeURLSchemeTask:key];
}
- (int) processURLRequestReadBodyStream:(unsigned long long)requestId :(void *)buf :(int)bufLen {
int res = 0;
int *pRes = &res;
bool hasRequest = [self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> urlSchemeTask) {
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
if (!stream) {
*pRes = -3;
} else {
NSStreamStatus status = stream.streamStatus;
if (status == NSStreamStatusAtEnd) {
*pRes = 0;
} else if (status != NSStreamStatusOpen) {
*pRes = -4;
} else if (!stream.hasBytesAvailable) {
*pRes = 0;
} else {
*pRes = [stream read:buf maxLength:bufLen];
} }
} }
} @finally { }];
[self.urlRequestsLock lock];
[self.urlRequests removeObjectForKey:key]; // This will release the urlSchemeTask which was retained from the dictionary if (!hasRequest) {
[self.urlRequestsLock unlock]; res = -2;
} }
return res;
} }
- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask { - (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
@ -467,6 +487,7 @@
const char *headerJSON = ""; const char *headerJSON = "";
const void *body = nil; const void *body = nil;
int bodyLen = 0; int bodyLen = 0;
int hasBodyStream = 0;
NSData *headers = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil]; NSData *headers = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil];
if (headers) { if (headers) {
@ -477,23 +498,85 @@
if (urlSchemeTask.request.HTTPBody) { if (urlSchemeTask.request.HTTPBody) {
body = urlSchemeTask.request.HTTPBody.bytes; body = urlSchemeTask.request.HTTPBody.bytes;
bodyLen = urlSchemeTask.request.HTTPBody.length; bodyLen = urlSchemeTask.request.HTTPBody.length;
} else { } else if (urlSchemeTask.request.HTTPBodyStream) {
// TODO handle HTTPBodyStream hasBodyStream = 1;
[urlSchemeTask.request.HTTPBodyStream open];
} }
[self.urlRequestsLock lock]; unsigned long long requestId;
self.urlRequestsId++; @synchronized(self.urlRequests) {
unsigned long long requestId = self.urlRequestsId; self.urlRequestsId++;
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId]; requestId = self.urlRequestsId;
self.urlRequests[key] = urlSchemeTask; NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
[self.urlRequestsLock unlock]; self.urlRequests[key] = urlSchemeTask;
}
processURLRequest(self, requestId, url, method, headerJSON, body, bodyLen); processURLRequest(self, requestId, url, method, headerJSON, body, bodyLen, hasBodyStream);
} }
- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask { - (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
// TODO implement the stopping process here in a better way... NSArray<NSNumber*> *keys;
// As soon as we introduce response body streaming we need to rewrite this nevertheless. @synchronized(self.urlRequests) {
keys = [self.urlRequests allKeys];
}
for (NSNumber *key in keys) {
if (self.urlRequests[key] == urlSchemeTask) {
[self removeURLSchemeTask:key];
}
}
}
- (void) removeURLSchemeTask:(NSNumber *)urlSchemeTaskKey {
id<WKURLSchemeTask> urlSchemeTask = nil;
@synchronized(self.urlRequests) {
urlSchemeTask = self.urlRequests[urlSchemeTaskKey];
}
if (!urlSchemeTask) {
return;
}
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
if (stream) {
[stream close];
}
@synchronized(self.urlRequests) {
[self.urlRequests removeObjectForKey:urlSchemeTaskKey];
}
}
- (bool)processURLSchemeTaskCall:(unsigned long long)requestId :(schemeTaskCaller)fn {
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
id<WKURLSchemeTask> urlSchemeTask;
@synchronized(self.urlRequests) {
urlSchemeTask = self.urlRequests[key];
}
if (urlSchemeTask == nil) {
// Stopped task, drop content...
return false;
}
@try {
fn(urlSchemeTask);
} @catch (NSException *exception) {
[self removeURLSchemeTask:key];
// This is very bad to detect a stopped schemeTask this should be implemented in a better way
// But it seems to be very tricky to not deadlock when keeping a lock curing executing fn()
// It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want
// to get the lock again to start another request or stop it.
if ([exception.reason isEqualToString: @"This task has already been stopped"]) {
return false;
}
@throw exception;
}
return true;
} }
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

View File

@ -14,18 +14,14 @@ package darwin
*/ */
import "C" import "C"
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"io"
"log" "log"
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"strconv" "strconv"
"unsafe"
"github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/internal/frontend"
@ -37,7 +33,7 @@ import (
const startURL = "wails://wails/" const startURL = "wails://wails/"
var messageBuffer = make(chan string, 100) var messageBuffer = make(chan string, 100)
var requestBuffer = make(chan *request, 100) var requestBuffer = make(chan *wkWebViewRequest, 100)
var callbackBuffer = make(chan uint, 10) var callbackBuffer = make(chan uint, 10)
type Frontend struct { type Frontend struct {
@ -96,7 +92,10 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
} }
result.assets = assets result.assets = assets
go result.startRequestProcessor() // Start 10 processors to handle requests in parallel
for i := 0; i < 10; i++ {
go result.startRequestProcessor()
}
} }
go result.startMessageProcessor() go result.startMessageProcessor()
@ -342,8 +341,10 @@ func (f *Frontend) ExecJS(js string) {
f.mainWindow.ExecJS(js) f.mainWindow.ExecJS(js)
} }
func (f *Frontend) processRequest(r *request) { func (f *Frontend) processRequest(r *wkWebViewRequest) {
rw := httptest.NewRecorder() rw := &wkWebViewResponseWriter{r: r}
defer rw.Close()
f.assets.ProcessHTTPRequest( f.assets.ProcessHTTPRequest(
r.url, r.url,
rw, rw,
@ -363,28 +364,6 @@ func (f *Frontend) processRequest(r *request) {
return req, nil return req, nil
}, },
) )
header := map[string]string{}
for k := range rw.Header() {
header[k] = rw.Header().Get(k)
}
headerData, _ := json.Marshal(header)
var content unsafe.Pointer
var contentLen int
if _contents := rw.Body.Bytes(); _contents != nil {
content = unsafe.Pointer(&_contents[0])
contentLen = len(_contents)
}
var headers unsafe.Pointer
var headersLen int
if len(headerData) != 0 {
headers = unsafe.Pointer(&headerData[0])
headersLen = len(headerData)
}
C.ProcessURLResponse(r.ctx, r.id, C.int(rw.Code), headers, C.int(headersLen), content, C.int(contentLen))
} }
//func (f *Frontend) processSystemEvent(message string) { //func (f *Frontend) processSystemEvent(message string) {
@ -403,64 +382,12 @@ func (f *Frontend) processRequest(r *request) {
// } // }
//} //}
type request struct {
id C.ulonglong
url string
method string
headers string
body []byte
ctx unsafe.Pointer
}
func (r *request) GetHttpRequest() (*http.Request, error) {
var body io.Reader
if len(r.body) != 0 {
body = bytes.NewReader(r.body)
}
req, err := http.NewRequest(r.method, r.url, body)
if err != nil {
return nil, err
}
if r.headers != "" {
var h map[string]string
if err := json.Unmarshal([]byte(r.headers), &h); err != nil {
return nil, fmt.Errorf("Unable to unmarshal request headers: %s", err)
}
for k, v := range h {
req.Header.Add(k, v)
}
}
return req, nil
}
//export processMessage //export processMessage
func processMessage(message *C.char) { func processMessage(message *C.char) {
goMessage := C.GoString(message) goMessage := C.GoString(message)
messageBuffer <- goMessage messageBuffer <- goMessage
} }
//export processURLRequest
func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) {
var goBody []byte
if body != nil && bodyLen != 0 {
goBody = C.GoBytes(body, bodyLen)
}
requestBuffer <- &request{
id: requestId,
url: C.GoString(url),
method: C.GoString(method),
headers: C.GoString(headers),
body: goBody,
ctx: ctx,
}
}
//export processCallback //export processCallback
func processCallback(callbackID uint) { func processCallback(callbackID uint) {
callbackBuffer <- callbackID callbackBuffer <- callbackID

View File

@ -15,7 +15,7 @@ extern "C"
#endif #endif
void processMessage(const char *); void processMessage(const char *);
void processURLRequest(void*, unsigned long long, const char *, const char *, const char *, const void *, int); void processURLRequest(void*, unsigned long long, const char *, const char *, const char *, const void *, int, int);
void processMessageDialogResponse(int); void processMessageDialogResponse(int);
void processOpenFileDialogResponse(const char*); void processOpenFileDialogResponse(const char*);
void processSaveFileDialogResponse(const char*); void processSaveFileDialogResponse(const char*);

View File

@ -0,0 +1,106 @@
//go:build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import "Application.h"
#include <stdlib.h>
*/
import "C"
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"unsafe"
)
//export processURLRequest
func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int, hasBodyStream C.int) {
var bodyReader io.Reader
if body != nil && bodyLen != 0 {
bodyReader = bytes.NewReader(C.GoBytes(body, bodyLen))
} else if hasBodyStream != 0 {
bodyReader = &bodyStreamReader{id: requestId, ctx: ctx}
}
requestBuffer <- &wkWebViewRequest{
id: requestId,
url: C.GoString(url),
method: C.GoString(method),
headers: C.GoString(headers),
body: bodyReader,
ctx: ctx,
}
}
type wkWebViewRequest struct {
id C.ulonglong
url string
method string
headers string
body io.Reader
ctx unsafe.Pointer
}
func (r *wkWebViewRequest) GetHttpRequest() (*http.Request, error) {
req, err := http.NewRequest(r.method, r.url, r.body)
if err != nil {
return nil, err
}
if r.headers != "" {
var h map[string]string
if err := json.Unmarshal([]byte(r.headers), &h); err != nil {
return nil, fmt.Errorf("unable to unmarshal request headers: %s", err)
}
for k, v := range h {
req.Header.Add(k, v)
}
}
return req, nil
}
var _ io.Reader = &bodyStreamReader{}
type bodyStreamReader struct {
id C.ulonglong
ctx unsafe.Pointer
}
// Read implements io.Reader
func (r *bodyStreamReader) Read(p []byte) (n int, err error) {
var content unsafe.Pointer
var contentLen int
if p != nil {
content = unsafe.Pointer(&p[0])
contentLen = len(p)
}
res := C.ProcessURLRequestReadBodyStream(r.ctx, r.id, content, C.int(contentLen))
if res > 0 {
return int(res), nil
}
switch res {
case 0:
return 0, io.EOF
case -1:
return 0, fmt.Errorf("body: stream error")
case -2:
return 0, errRequestStopped
case -3:
return 0, fmt.Errorf("body: no stream defined")
case -4:
return 0, io.ErrClosedPipe
default:
return 0, fmt.Errorf("body: unknown error %d", res)
}
}

View File

@ -0,0 +1,81 @@
//go:build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import "Application.h"
#include <stdlib.h>
*/
import "C"
import (
"encoding/json"
"errors"
"net/http"
"unsafe"
)
var (
errRequestStopped = errors.New("request has been stopped")
)
type wkWebViewResponseWriter struct {
r *wkWebViewRequest
header http.Header
wroteHeader bool
}
func (rw *wkWebViewResponseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *wkWebViewResponseWriter) Write(buf []byte) (int, error) {
rw.WriteHeader(http.StatusOK)
var content unsafe.Pointer
var contentLen int
if buf != nil {
content = unsafe.Pointer(&buf[0])
contentLen = len(buf)
}
if !C.ProcessURLDidReceiveData(rw.r.ctx, rw.r.id, content, C.int(contentLen)) {
return 0, errRequestStopped
}
return contentLen, nil
}
func (rw *wkWebViewResponseWriter) WriteHeader(code int) {
if rw.wroteHeader {
return
}
rw.wroteHeader = true
header := map[string]string{}
for k := range rw.Header() {
header[k] = rw.Header().Get(k)
}
headerData, _ := json.Marshal(header)
var headers unsafe.Pointer
var headersLen int
if len(headerData) != 0 {
headers = unsafe.Pointer(&headerData[0])
headersLen = len(headerData)
}
C.ProcessURLDidReceiveResponse(rw.r.ctx, rw.r.id, C.int(code), headers, C.int(headersLen))
}
func (rw *wkWebViewResponseWriter) Close() {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
C.ProcessURLDidFinish(rw.r.ctx, rw.r.id)
}

View File

@ -258,11 +258,11 @@ Not all features of an `http.Request` are currently supported, please see the fo
| DELETE | ✅ | ✅ | ✅ [^1] | | DELETE | ✅ | ✅ | ✅ [^1] |
| Request Headers | ✅ | ✅ | ✅ [^1] | | Request Headers | ✅ | ✅ | ✅ [^1] |
| Request Body | ✅ | ✅ | ❌ | | Request Body | ✅ | ✅ | ❌ |
| Request Body Streaming | ❌ | ❌ | ❌ | | Request Body Streaming | ✅ | ✅ | ❌ |
| Response StatusCodes | ✅ | ✅ | ✅ [^1] | | Response StatusCodes | ✅ | ✅ | ✅ [^1] |
| Response Headers | ✅ | ✅ | ✅ [^1] | | Response Headers | ✅ | ✅ | ✅ [^1] |
| Response Body | ✅ | ✅ | ✅ | | Response Body | ✅ | ✅ | ✅ |
| Response Body Streaming | ❌ | | ✅ | | Response Body Streaming | ❌ | | ✅ |
| WebSockets | ❌ | ❌ | ❌ | | WebSockets | ❌ | ❌ | ❌ |
| HTTP Redirects 30x | ✅ | ❌ | ❌ | | HTTP Redirects 30x | ✅ | ❌ | ❌ |

View File

@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The [AssetServer](/docs/reference/options#assetserver) now supports serving the index.html file when requesting a directory. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2110) - The [AssetServer](/docs/reference/options#assetserver) now supports serving the index.html file when requesting a directory. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2110)
- Added support for WebKit2GTK 2.36+ on Linux. This brings additional features for the [AssetServer](/docs/reference/options#assetserver), like support for HTTP methods and Headers. The app must be compiled with the Go build tag `webkit2_36` to activate support for this features. This also bumps the minimum requirement of WebKit2GTK to 2.36 for your app. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2151) - Added support for WebKit2GTK 2.36+ on Linux. This brings additional features for the [AssetServer](/docs/reference/options#assetserver), like support for HTTP methods and Headers. The app must be compiled with the Go build tag `webkit2_36` to activate support for this features. This also bumps the minimum requirement of WebKit2GTK to 2.36 for your app. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2151)
- Added support for file input selection on macOS. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2209) - Added support for file input selection on macOS. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2209)
- Added support Request/Response streaming of the AssetServer on macOS. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2219)
### Fixed ### Fixed
- The `noreload` flag in wails dev wasn't applied. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2081) - The `noreload` flag in wails dev wasn't applied. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2081)