Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Examples/WasmClient/JSURL.swift
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 27 additions & 12 deletions Examples/WasmClient/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import AsyncStreaming
import BasicContainers
import ContainersPreview
import FetchHTTPClient
import Foundation
import HTTPAPIs
import JavaScriptEventLoop
import JavaScriptKit
Expand All @@ -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
Expand All @@ -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"
]
Expand Down Expand Up @@ -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")
}
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down Expand Up @@ -290,6 +290,9 @@ if enableWasm {
],
path: "Examples/WasmClient",
swiftSettings: wasmExtraSettings,
linkerSettings: [
.linkedLibrary("swiftUnicodeDataTables", .when(platforms: [.wasi]))
],
plugins: [
.plugin(name: "BridgeJS", package: "JavaScriptKit")
],
Expand Down
6 changes: 3 additions & 3 deletions Sources/FetchHTTPClient/FetchHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
//===----------------------------------------------------------------------===//

public import BasicContainers
import Foundation
@_exported public import HTTPAPIs
import JavaScriptEventLoop
import JavaScriptKit
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//
//===----------------------------------------------------------------------===//

#if !hasFeature(Embedded)
import AsyncStreaming
import BasicContainers

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -254,3 +265,4 @@ where
}

struct LengthLimitExceededError: Error {}
#endif // !hasFeature(Embedded)
2 changes: 2 additions & 0 deletions Sources/HTTPAPIs/Client/HTTPClientRequestBody+Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//
//===----------------------------------------------------------------------===//

#if !hasFeature(Embedded)
import BasicContainers

#if canImport(FoundationEssentials)
Expand All @@ -34,3 +35,4 @@ extension HTTPClientRequestBody where Writer: ~Copyable {
}
}
}
#endif // !hasFeature(Embedded)
Loading