diff --git a/Examples/WasmClient/JSURL.swift b/Examples/WasmClient/JSURL.swift new file mode 100644 index 0000000..d99afea --- /dev/null +++ b/Examples/WasmClient/JSURL.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP API Proposal open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// https://developer.mozilla.org/en-US/docs/Web/API/URL +@JSClass(jsName: "URL", from: .global) struct JSURL { + @JSFunction init(_ url: String) throws(JSException) + @JSGetter var `protocol`: String + @JSGetter var host: String + @JSGetter var pathname: String + @JSGetter var search: String +} diff --git a/Examples/WasmClient/main.swift b/Examples/WasmClient/main.swift index aa87fb6..3a3c776 100644 --- a/Examples/WasmClient/main.swift +++ b/Examples/WasmClient/main.swift @@ -15,7 +15,6 @@ import AsyncStreaming import BasicContainers import ContainersPreview import FetchHTTPClient -import Foundation import HTTPAPIs import JavaScriptEventLoop import JavaScriptKit @@ -27,18 +26,30 @@ JavaScriptEventLoop.installGlobalExecutor() let client = FetchHTTPClient() let status = Status() -// Ask the user for the URL string. -let urlString = try prompt("URL:", "http://localhost:8000/").trimmingCharacters(in: .whitespacesAndNewlines) -guard let url = URL(string: urlString) else { - status.set("❌ Not a valid URL") +// This browser demo has no graceful recovery path, so bad input is reported on +// the status line and then aborts. +func fail(_ message: String) -> Never { + status.set(message) fatalError() } -// Parse the method -let methodString = try prompt("Method (GET, POST, etc.):", "GET").trimmingCharacters(in: .whitespacesAndNewlines).uppercased() +// Parse the user-entered URL with the host's WHATWG parser (imported via +// BridgeJS). Invalid input throws. +let urlString = try prompt("URL:", "http://localhost:8000/") +let url: JSURL +do { + url = try JSURL(urlString) +} catch { + fail("❌ Not a valid URL") +} + +let scheme = String(try url.`protocol`.dropLast()) +let authority = try url.host +let path = try url.pathname + url.search + +let methodString = try prompt("Method (GET, POST, etc.):", "GET").uppercased() guard let method = HTTPRequest.Method(methodString) else { - status.set("❌ Not a valid method") - fatalError() + fail("❌ Not a valid method") } // Optionally accept a body @@ -53,13 +64,15 @@ if method == .post || method == .put { } } -status.set("⏳ Making \(method) request to \(url)") +status.set("⏳ Making \(method) request to \(urlString)") do { try await client.perform( request: .init( method: method, - url: url, + scheme: scheme, + authority: authority, + path: path, headerFields: [ .init("Client")!: "Swift-Wasm" ] @@ -105,5 +118,7 @@ do { } } } catch { - status.set("❌ Fetch failed: \(error)") + // Embedded Swift can't reflect over an existential `any Error`, so report a + // fixed message rather than interpolating `error`. + status.set("❌ Fetch failed") } diff --git a/Package.swift b/Package.swift index a23950a..fb6191c 100644 --- a/Package.swift +++ b/Package.swift @@ -256,7 +256,7 @@ if enableWasm { ] package.dependencies.append( - .package(url: "https://github.com/swiftwasm/JavaScriptKit.git", from: "0.53.0") + .package(url: "https://github.com/swiftwasm/JavaScriptKit.git", from: "0.56.1") ) package.products.append( .library(name: "FetchHTTPClient", targets: ["FetchHTTPClient"]) @@ -290,6 +290,9 @@ if enableWasm { ], path: "Examples/WasmClient", swiftSettings: wasmExtraSettings, + linkerSettings: [ + .linkedLibrary("swiftUnicodeDataTables", .when(platforms: [.wasi])) + ], plugins: [ .plugin(name: "BridgeJS", package: "JavaScriptKit") ], diff --git a/Sources/FetchHTTPClient/FetchHTTPClient.swift b/Sources/FetchHTTPClient/FetchHTTPClient.swift index 837cc9f..43adef9 100644 --- a/Sources/FetchHTTPClient/FetchHTTPClient.swift +++ b/Sources/FetchHTTPClient/FetchHTTPClient.swift @@ -12,7 +12,6 @@ //===----------------------------------------------------------------------===// public import BasicContainers -import Foundation @_exported public import HTTPAPIs import JavaScriptEventLoop import JavaScriptKit @@ -54,9 +53,10 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient { options: RequestOptions, responseHandler: nonisolated(nonsending) (HTTPTypes.HTTPResponse, consuming ResponseBodyReader) async throws -> Return ) async throws -> Return where Return: ~Copyable { - guard let url = request.url else { + guard let scheme = request.scheme, let authority = request.authority, let path = request.path else { throw FetchError.BadURL } + let urlString = "\(scheme)://\(authority)\(path)" var jsBody: JSObject? = nil @@ -80,7 +80,7 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient { // Perform the request let requestInit = RequestInit(body: jsBody, method: request.method.rawValue, headers: requestHeaders) - let response = try await fetch(url.absoluteString, requestInit) + let response = try await fetch(urlString, requestInit) let responseStatus = try response.status let responseStatusText = try response.statusText let stream = try response.body diff --git a/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift b/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift index 165d1e8..fd2fea4 100644 --- a/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift +++ b/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift @@ -11,6 +11,7 @@ // //===----------------------------------------------------------------------===// +#if !hasFeature(Embedded) import AsyncStreaming import BasicContainers @@ -21,6 +22,7 @@ public import struct FoundationEssentials.Data public import struct Foundation.URL public import struct Foundation.Data #endif +#endif // !hasFeature(Embedded) @available(anyAppleOS 26.0, *) extension HTTPClient @@ -53,7 +55,16 @@ where let options = options ?? self.defaultRequestOptions return try await self.perform(request: request, body: body, options: options, responseHandler: responseHandler) } +} +#if !hasFeature(Embedded) +@available(anyAppleOS 26.0, *) +extension HTTPClient +where + Self: ~Copyable & ~Escapable, + Reader: ~Copyable, + Writer: ~Copyable +{ /// Performs an HTTP GET request and collects the response body. /// /// This convenience method executes a GET request to the specified URL and collects @@ -254,3 +265,4 @@ where } struct LengthLimitExceededError: Error {} +#endif // !hasFeature(Embedded) diff --git a/Sources/HTTPAPIs/Client/HTTPClientRequestBody+Data.swift b/Sources/HTTPAPIs/Client/HTTPClientRequestBody+Data.swift index 03f3db8..f54b6dc 100644 --- a/Sources/HTTPAPIs/Client/HTTPClientRequestBody+Data.swift +++ b/Sources/HTTPAPIs/Client/HTTPClientRequestBody+Data.swift @@ -11,6 +11,7 @@ // //===----------------------------------------------------------------------===// +#if !hasFeature(Embedded) import BasicContainers #if canImport(FoundationEssentials) @@ -34,3 +35,4 @@ extension HTTPClientRequestBody where Writer: ~Copyable { } } } +#endif // !hasFeature(Embedded)