## Summary

I've successfully implemented an HTTP API for Wails v3 that provides a workaround for the CORS issue. Here's what was created:

### Backend Implementation:
1. **`http.go`** - Core HTTP functionality with:
   - Support for GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS methods
   - Request/response type definitions
   - Error handling and timeout support
   - Automatic User-Agent header

2. **`messageprocessor_http.go`** - Message processor for handling HTTP requests from frontend

3. **Updated `messageprocessor.go`** - Added HTTP request constant and routing

### Frontend Implementation:
1. **`http.js`** - JavaScript runtime API with:
   - `Fetch()` - Generic method for full control
   - Convenience methods: `Get()`, `Post()`, `Put()`, `Delete()`, `Patch()`, `Head()`
   - Automatic JSON serialization for POST/PUT/PATCH bodies
   - Promise-based API

2. **Updated runtime files**:
   - Added HTTP to object names in `runtime.js`
   - Added support for POST body in runtime calls
   - Exported HTTP module in `main.js` and `api/index.js`

### Example Application:
Created a complete example in `v3/examples/http-cors-workaround/` demonstrating:
- GET/POST requests
- Custom headers
- Error handling
- Timeout functionality

### How It Works:
Instead of making requests directly from the frontend (which fails due to CORS), the HTTP API:
1. Sends the request details to the Go backend
2. Go makes the actual HTTP request (no CORS restrictions)
3. Returns the response to the frontend

This completely bypasses CORS issues while maintaining a familiar API similar to fetch/axios.
This commit is contained in:
Lea Anthony 2025-07-30 22:42:44 +10:00
commit e510f8d897
11 changed files with 641 additions and 4 deletions

View file

@ -0,0 +1,77 @@
# HTTP CORS Workaround Example
This example demonstrates how to use the Wails v3 HTTP runtime API to bypass CORS issues when making network requests from the frontend.
## The Problem
When using Wails with the `wails://wails` protocol on Linux/macOS, CORS restrictions prevent direct HTTP requests from the frontend to external APIs. This is because:
1. Wails uses a custom protocol (`wails://wails`) which is not recognized by external servers
2. The browser sends an empty Origin header for custom protocols
3. External servers reject requests without a valid Origin header
## The Solution
The Wails HTTP runtime API allows you to make HTTP requests through the Go backend, completely bypassing CORS restrictions.
## Usage
Instead of using `fetch` or `axios` directly:
```javascript
// This will fail with CORS error
const response = await fetch('https://api.example.com/data');
```
Use the Wails HTTP API:
```javascript
// This works - request is made from Go backend
const response = await wails.HTTP.Get('https://api.example.com/data');
```
## API Methods
- `wails.HTTP.Get(url, options)` - GET request
- `wails.HTTP.Post(url, body, options)` - POST request
- `wails.HTTP.Put(url, body, options)` - PUT request
- `wails.HTTP.Delete(url, options)` - DELETE request
- `wails.HTTP.Patch(url, body, options)` - PATCH request
- `wails.HTTP.Head(url, options)` - HEAD request
- `wails.HTTP.Fetch(options)` - Generic request with full options
## Example with Headers and Timeout
```javascript
const response = await wails.HTTP.Post('https://api.example.com/users', {
name: 'John Doe',
email: 'john@example.com'
}, {
headers: {
'Authorization': 'Bearer token123',
'X-Custom-Header': 'value'
},
timeout: 10 // 10 seconds timeout
});
if (response.error) {
console.error('Request failed:', response.error);
} else {
console.log('Status:', response.statusCode);
console.log('Data:', JSON.parse(response.body));
}
```
## Running the Example
1. Navigate to this directory
2. Run `wails3 dev`
3. Click the buttons to test various HTTP methods
4. Check the console for responses
## Notes
- All requests are made from the Go backend, so there are no CORS restrictions
- The response includes `statusCode`, `headers`, `body`, and `error` (if any)
- Request bodies are automatically JSON stringified for objects
- Default timeout is 30 seconds, but can be customized

View file

@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTTP CORS Workaround Example</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
}
.section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
pre {
background-color: #f4f4f4;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
.error {
color: #dc3545;
}
.success {
color: #28a745;
}
#loading {
display: none;
color: #666;
}
</style>
</head>
<body>
<h1>HTTP CORS Workaround Example</h1>
<div class="section">
<h2>Test HTTP Requests</h2>
<p>Click the buttons below to test various HTTP methods using the Wails HTTP API:</p>
<button onclick="testGet()">Test GET Request</button>
<button onclick="testPost()">Test POST Request</button>
<button onclick="testWithHeaders()">Test with Custom Headers</button>
<button onclick="testError()">Test Error Handling</button>
<button onclick="testTimeout()">Test Timeout</button>
<div id="loading">Loading...</div>
</div>
<div class="section">
<h2>Response:</h2>
<pre id="response">Click a button to make a request</pre>
</div>
<script>
// Wait for Wails to be ready
window.addEventListener('DOMContentLoaded', () => {
console.log('Wails HTTP API Example Ready');
});
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
function showResponse(data, isError = false) {
const responseEl = document.getElementById('response');
responseEl.textContent = JSON.stringify(data, null, 2);
responseEl.className = isError ? 'error' : 'success';
}
async function testGet() {
showLoading(true);
try {
const response = await wails.HTTP.Get('https://jsonplaceholder.typicode.com/posts/1');
if (response.error) {
showResponse({ error: response.error }, true);
} else {
showResponse({
statusCode: response.statusCode,
data: JSON.parse(response.body)
});
}
} catch (error) {
showResponse({ error: error.message }, true);
} finally {
showLoading(false);
}
}
async function testPost() {
showLoading(true);
try {
const postData = {
title: 'Test Post from Wails',
body: 'This post was made using the Wails HTTP API',
userId: 1
};
const response = await wails.HTTP.Post('https://jsonplaceholder.typicode.com/posts', postData);
if (response.error) {
showResponse({ error: response.error }, true);
} else {
showResponse({
statusCode: response.statusCode,
data: JSON.parse(response.body)
});
}
} catch (error) {
showResponse({ error: error.message }, true);
} finally {
showLoading(false);
}
}
async function testWithHeaders() {
showLoading(true);
try {
const response = await wails.HTTP.Fetch({
url: 'https://httpbin.org/headers',
method: 'GET',
headers: {
'X-Custom-Header': 'Wails-HTTP-API',
'Accept': 'application/json'
}
});
if (response.error) {
showResponse({ error: response.error }, true);
} else {
showResponse({
statusCode: response.statusCode,
data: JSON.parse(response.body)
});
}
} catch (error) {
showResponse({ error: error.message }, true);
} finally {
showLoading(false);
}
}
async function testError() {
showLoading(true);
try {
const response = await wails.HTTP.Get('https://httpstat.us/500');
showResponse({
statusCode: response.statusCode,
body: response.body,
error: response.error
}, response.statusCode >= 400);
} catch (error) {
showResponse({ error: error.message }, true);
} finally {
showLoading(false);
}
}
async function testTimeout() {
showLoading(true);
try {
const response = await wails.HTTP.Fetch({
url: 'https://httpstat.us/200?sleep=5000',
method: 'GET',
timeout: 2 // 2 second timeout
});
if (response.error) {
showResponse({ error: response.error }, true);
} else {
showResponse({
statusCode: response.statusCode,
body: response.body
});
}
} catch (error) {
showResponse({ error: error.message }, true);
} finally {
showLoading(false);
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,33 @@
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed assets
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "HTTP CORS Workaround Example",
Description: "Demonstrates using Wails HTTP API to bypass CORS",
Assets: application.AssetOptions{
FS: assets,
},
})
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "HTTP CORS Workaround",
Width: 800,
Height: 600,
URL: "/",
})
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View file

@ -0,0 +1 @@
export * from '../http';

View file

@ -15,8 +15,9 @@ import * as Screens from "./screens";
import * as Dialogs from "./dialogs";
import * as Events from "./events";
import * as Window from "./window";
import * as HTTP from "./http";
export { Clipboard, Application, Screens, Dialogs, Events, Window };
export { Clipboard, Application, Screens, Dialogs, Events, Window, HTTP };
/**
* Call a plugin method

View file

@ -0,0 +1,134 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import {newRuntimeCallerWithID, objectNames} from "./runtime";
let call = newRuntimeCallerWithID(objectNames.HTTP);
let HTTPFetch = 0;
/**
* Perform an HTTP request
* @param {Object} options - The request options
* @param {string} options.url - The URL to request
* @param {string} [options.method='GET'] - The HTTP method
* @param {Object} [options.headers] - Request headers
* @param {string} [options.body] - Request body
* @param {number} [options.timeout] - Request timeout in seconds
* @returns {Promise<Object>} The response object
*/
export function Fetch(options) {
// Ensure we have required fields
if (!options || !options.url) {
return Promise.reject(new Error("URL is required"));
}
// Set defaults
const request = {
url: options.url,
method: options.method || 'GET',
headers: options.headers || {},
body: options.body || '',
timeout: options.timeout || 30
};
// For POST requests, we need to send the body differently
if (request.body) {
return call(HTTPFetch, null, JSON.stringify(request));
}
return call(HTTPFetch, null, JSON.stringify(request));
}
/**
* Convenience method for GET requests
* @param {string} url - The URL to request
* @param {Object} [options] - Additional options
* @returns {Promise<Object>} The response object
*/
export function Get(url, options = {}) {
return Fetch({ ...options, url, method: 'GET' });
}
/**
* Convenience method for POST requests
* @param {string} url - The URL to request
* @param {string|Object} body - The request body
* @param {Object} [options] - Additional options
* @returns {Promise<Object>} The response object
*/
export function Post(url, body, options = {}) {
if (typeof body === 'object' && !(body instanceof String)) {
body = JSON.stringify(body);
options.headers = {
'Content-Type': 'application/json',
...(options.headers || {})
};
}
return Fetch({ ...options, url, method: 'POST', body });
}
/**
* Convenience method for PUT requests
* @param {string} url - The URL to request
* @param {string|Object} body - The request body
* @param {Object} [options] - Additional options
* @returns {Promise<Object>} The response object
*/
export function Put(url, body, options = {}) {
if (typeof body === 'object' && !(body instanceof String)) {
body = JSON.stringify(body);
options.headers = {
'Content-Type': 'application/json',
...(options.headers || {})
};
}
return Fetch({ ...options, url, method: 'PUT', body });
}
/**
* Convenience method for DELETE requests
* @param {string} url - The URL to request
* @param {Object} [options] - Additional options
* @returns {Promise<Object>} The response object
*/
export function Delete(url, options = {}) {
return Fetch({ ...options, url, method: 'DELETE' });
}
/**
* Convenience method for PATCH requests
* @param {string} url - The URL to request
* @param {string|Object} body - The request body
* @param {Object} [options] - Additional options
* @returns {Promise<Object>} The response object
*/
export function Patch(url, body, options = {}) {
if (typeof body === 'object' && !(body instanceof String)) {
body = JSON.stringify(body);
options.headers = {
'Content-Type': 'application/json',
...(options.headers || {})
};
}
return Fetch({ ...options, url, method: 'PATCH', body });
}
/**
* Convenience method for HEAD requests
* @param {string} url - The URL to request
* @param {Object} [options] - Additional options
* @returns {Promise<Object>} The response object
*/
export function Head(url, options = {}) {
return Fetch({ ...options, url, method: 'HEAD' });
}

View file

@ -15,6 +15,7 @@ import * as Application from './application';
import * as Screens from './screens';
import * as System from './system';
import * as Browser from './browser';
import * as HTTP from './http';
import {Plugin, Call, callErrorCallback, callCallback, CallByID, CallByName} from "./calls";
import {clientId} from './runtime';
import {newWindow} from "./window";
@ -61,6 +62,7 @@ export function newRuntime(windowName) {
System,
Screens,
Browser,
HTTP,
Call,
CallByID,
CallByName,

View file

@ -24,6 +24,7 @@ export const objectNames = {
Screens: 7,
System: 8,
Browser: 9,
HTTP: 10,
}
export let clientId = nanoid();
@ -67,7 +68,7 @@ export function newRuntimeCaller(object, windowName) {
};
}
function runtimeCallWithID(objectID, method, windowName, args) {
function runtimeCallWithID(objectID, method, windowName, args, body) {
let url = new URL(runtimeURL);
url.searchParams.append("object", objectID);
url.searchParams.append("method", method);
@ -80,6 +81,11 @@ function runtimeCallWithID(objectID, method, windowName, args) {
if (args) {
url.searchParams.append("args", JSON.stringify(args));
}
if (body) {
fetchOptions.method = "POST";
fetchOptions.body = body;
fetchOptions.headers["Content-Type"] = "application/json";
}
fetchOptions.headers["x-wails-client-id"] = clientId;
return new Promise((resolve, reject) => {
fetch(url, fetchOptions)
@ -100,7 +106,7 @@ function runtimeCallWithID(objectID, method, windowName, args) {
}
export function newRuntimeCallerWithID(object, windowName) {
return function (method, args=null) {
return runtimeCallWithID(object, method, windowName, args);
return function (method, args=null, body=null) {
return runtimeCallWithID(object, method, windowName, args, body);
};
}

126
v3/pkg/application/http.go Normal file
View file

@ -0,0 +1,126 @@
package application
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// HTTPRequest represents an HTTP request from the frontend
type HTTPRequest struct {
Method string `json:"method"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
Body string `json:"body,omitempty"`
Timeout int `json:"timeout,omitempty"` // timeout in seconds
}
// HTTPResponse represents an HTTP response to send back to the frontend
type HTTPResponse struct {
StatusCode int `json:"statusCode"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
Error string `json:"error,omitempty"`
}
// PerformHTTPRequest performs an HTTP request on behalf of the frontend
func PerformHTTPRequest(request HTTPRequest) HTTPResponse {
// Validate method
method := strings.ToUpper(request.Method)
if method != "GET" && method != "POST" && method != "PUT" && method != "DELETE" && method != "PATCH" && method != "HEAD" && method != "OPTIONS" {
return HTTPResponse{
StatusCode: 0,
Error: fmt.Sprintf("Invalid HTTP method: %s", request.Method),
}
}
// Create HTTP client with timeout
timeout := 30 * time.Second
if request.Timeout > 0 {
timeout = time.Duration(request.Timeout) * time.Second
}
client := &http.Client{
Timeout: timeout,
}
// Create request
var body io.Reader
if request.Body != "" {
body = bytes.NewBufferString(request.Body)
}
req, err := http.NewRequestWithContext(context.Background(), method, request.URL, body)
if err != nil {
return HTTPResponse{
StatusCode: 0,
Error: fmt.Sprintf("Failed to create request: %v", err),
}
}
// Set headers
for key, value := range request.Headers {
req.Header.Set(key, value)
}
// Set default User-Agent if not provided
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Wails/3.0")
}
// Perform the request
resp, err := client.Do(req)
if err != nil {
return HTTPResponse{
StatusCode: 0,
Error: fmt.Sprintf("Request failed: %v", err),
}
}
defer resp.Body.Close()
// Read response body
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return HTTPResponse{
StatusCode: resp.StatusCode,
Error: fmt.Sprintf("Failed to read response body: %v", err),
}
}
// Build response headers
responseHeaders := make(map[string]string)
for key, values := range resp.Header {
if len(values) > 0 {
responseHeaders[key] = values[0]
}
}
return HTTPResponse{
StatusCode: resp.StatusCode,
Headers: responseHeaders,
Body: string(bodyBytes),
}
}
// parseHTTPRequest parses the HTTP request from JSON
func parseHTTPRequest(data string) (*HTTPRequest, error) {
var request HTTPRequest
err := json.Unmarshal([]byte(data), &request)
if err != nil {
return nil, fmt.Errorf("failed to parse HTTP request: %v", err)
}
// Validate required fields
if request.URL == "" {
return nil, fmt.Errorf("URL is required")
}
if request.Method == "" {
request.Method = "GET"
}
return &request, nil
}

View file

@ -22,6 +22,7 @@ const (
screensRequest = 7
systemRequest = 8
browserRequest = 9
httpRequest = 10
)
type MessageProcessor struct {
@ -142,6 +143,8 @@ func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *h
m.processSystemMethod(method, rw, r, targetWindow, params)
case browserRequest:
m.processBrowserMethod(method, rw, r, targetWindow, params)
case httpRequest:
m.processHTTPMethod(method, rw, r, targetWindow, params)
default:
m.httpError(rw, "Unknown runtime call: %d", object)
}

View file

@ -0,0 +1,50 @@
package application
import (
"encoding/json"
"io"
"net/http"
)
const (
httpFetch = 0
)
func (m *MessageProcessor) processHTTPMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
switch method {
case httpFetch:
m.httpFetch(rw, r, window)
default:
m.httpError(rw, "Unknown HTTP method: %d", method)
}
}
func (m *MessageProcessor) httpFetch(rw http.ResponseWriter, r *http.Request, window Window) {
// Read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
m.httpError(rw, "Failed to read request body: %v", err)
return
}
// Parse the HTTP request
request, err := parseHTTPRequest(string(body))
if err != nil {
m.httpError(rw, "Invalid HTTP request: %v", err)
return
}
// Perform the HTTP request
response := PerformHTTPRequest(*request)
// Marshal response to JSON
responseJSON, err := json.Marshal(response)
if err != nil {
m.httpError(rw, "Failed to marshal response: %v", err)
return
}
// Send response
rw.Header().Set("Content-Type", "application/json")
rw.Write(responseJSON)
}