Skip to content
11 changes: 11 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ jobs:
runner_pool: general
build_scheme: swift-http-types-Package

embedded-swift:
name: Build with Embedded Swift
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11
with:
enable_linux_checks: false
enable_macos_checks: false
enable_windows_checks: false
enable_wasm_sdk_build: true
enable_embedded_wasm_sdk_build: true
swift_flags: --target HTTPTypes

release-builds:
name: Release builds
uses: apple/swift-nio/.github/workflows/release_builds.yml@main
Expand Down
10 changes: 7 additions & 3 deletions Sources/HTTPTypes/HTTPField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ public struct HTTPField: Sendable, Hashable {
///
/// - Parameter body: The closure to be invoked with the buffer.
/// - Returns: Result of the `body` closure.
public func withUnsafeBytesOfValue<Result>(
_ body: (UnsafeBufferPointer<UInt8>) throws -> Result
) rethrows -> Result {
public func withUnsafeBytesOfValue<Result, Failure: Error>(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this API breakage real?

@MaxDesiatov MaxDesiatov Apr 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC this is not source-breaking, but very likely ABI-breaking.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. This is considered fine.

_ body: (UnsafeBufferPointer<UInt8>) throws(Failure) -> Result
) throws(Failure) -> Result {
try self.rawValue.withUnsafeBytes(body)
}

Expand Down Expand Up @@ -219,6 +219,8 @@ extension HTTPField: CustomStringConvertible {
}
}

#if !hasFeature(Embedded)

extension HTTPField: CustomPlaygroundDisplayConvertible {
public var playgroundDescription: Any {
self.description
Expand Down Expand Up @@ -261,6 +263,8 @@ extension HTTPField: Codable {
}
}

#endif

extension HTTPField {
static func isValidToken(_ token: some StringProtocol) -> Bool {
!token.isEmpty
Expand Down
4 changes: 4 additions & 0 deletions Sources/HTTPTypes/HTTPFieldName.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ extension HTTPField.Name: LosslessStringConvertible {
}
}

#if !hasFeature(Embedded)

extension HTTPField.Name: CustomPlaygroundDisplayConvertible {
public var playgroundDescription: Any {
self.description
Expand Down Expand Up @@ -148,6 +150,8 @@ extension HTTPField.Name: Codable {
}
}

#endif

extension HTTPField.Name {
static var method: Self { .init(rawName: ":method", canonicalName: ":method") }
static var scheme: Self { .init(rawName: ":scheme", canonicalName: ":scheme") }
Expand Down
47 changes: 35 additions & 12 deletions Sources/HTTPTypes/HTTPFields.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,26 @@ public struct HTTPFields: Sendable, Hashable {
required init() {
}

func withLock<Result>(_ body: () throws -> Result) rethrows -> Result {
#if canImport(Darwin) && !hasFeature(Embedded)
func withLock<Result, Failure: Error>(_ body: () throws(Failure) -> Result) throws(Failure) -> Result {
fatalError()
}
#else
#if !hasFeature(Embedded) || (os(WASI) && compiler(>=6.4))
let mutex = Mutex<Void>(())
#endif

final func withLock<Result, Failure: Error>(_ body: () throws(Failure) -> Result) throws(Failure) -> Result {
#if !hasFeature(Embedded) || (os(WASI) && compiler(>=6.4))
try self.mutex.withLock { _ throws(Failure) in
try body()
}
#else
// Mutex not available
try body()
Comment thread
guoye-zhang marked this conversation as resolved.
#endif
}
#endif

var ensureIndex: [String: (first: UInt16, last: UInt16)] {
self.withLock {
Expand Down Expand Up @@ -107,38 +124,38 @@ public struct HTTPFields: Sendable, Hashable {
}
}

#if canImport(Darwin) && !hasFeature(Embedded)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
private final class _StorageWithMutex: _Storage, @unchecked Sendable {
let mutex = Mutex<Void>(())

override func withLock<Result>(_ body: () throws -> Result) rethrows -> Result {
try self.mutex.withLock { _ in
override func withLock<Result, Failure: Error>(_ body: () throws(Failure) -> Result) throws(Failure) -> Result {
try self.mutex.withLock { _ throws(Failure) in
try body()
}
}
}

#if canImport(Darwin)
private final class _StorageWithNIOLock: _Storage, @unchecked Sendable {
let lock = LockStorage.create(value: ())

override func withLock<Result>(_ body: () throws -> Result) rethrows -> Result {
try self.lock.withLockedValue { _ in
override func withLock<Result, Failure: Error>(_ body: () throws(Failure) -> Result) throws(Failure) -> Result {
try self.lock.withLockedValue { _ throws(Failure) in
try body()
}
}
}
#endif

private var _storage = {
#if canImport(Darwin)
#if canImport(Darwin) && !hasFeature(Embedded)
if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) {
_StorageWithMutex()
} else {
_StorageWithNIOLock()
}
#else
_StorageWithMutex()
_Storage()
#endif
}()

Expand Down Expand Up @@ -166,13 +183,14 @@ public struct HTTPFields: Sendable, Hashable {
let fields = self.fields(for: name)
if fields.first(where: { _ in true }) != nil {
let separator = name == .cookie ? "; " : ", "
return fields.lazy.map(\.value).joined(separator: separator)
return fields.lazy.map { $0.value }.joined(separator: separator)
} else {
return nil
}
}
set {
if let newValue {
#if !hasFeature(Embedded)
if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *),
name == .cookie
{
Expand All @@ -182,9 +200,10 @@ public struct HTTPFields: Sendable, Hashable {
},
for: name
)
} else {
self.setFields(CollectionOfOne(HTTPField(name: name, value: newValue)), for: name)
return
}
#endif
self.setFields(CollectionOfOne(HTTPField(name: name, value: newValue)), for: name)
} else {
self.setFields(EmptyCollection(), for: name)
}
Expand All @@ -194,7 +213,7 @@ public struct HTTPFields: Sendable, Hashable {
/// Access the field values by name as an array of strings. The order of fields is preserved.
public subscript(values name: HTTPField.Name) -> [String] {
get {
self.fields(for: name).map(\.value)
self.fields(for: name).map { $0.value }
}
set {
self.setFields(newValue.lazy.map { HTTPField(name: name, value: $0) }, for: name)
Expand Down Expand Up @@ -355,6 +374,8 @@ extension HTTPFields: RangeReplaceableCollection, RandomAccessCollection, Mutabl
}
}

#if !hasFeature(Embedded)

extension HTTPFields: CustomDebugStringConvertible {
public var debugDescription: String {
self._storage.fields.description
Expand Down Expand Up @@ -385,6 +406,8 @@ extension HTTPFields: Codable {
}
}

#endif

extension Array {
// `removalIndices` must be ordered.
mutating func remove(at removalIndices: some Sequence<Index>) {
Expand Down
4 changes: 4 additions & 0 deletions Sources/HTTPTypes/HTTPParsedFields.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

#if !hasFeature(Embedded) || compiler(>=6.4)

struct HTTPParsedFields {
private var method: ISOLatin1String?
private var scheme: ISOLatin1String?
Expand Down Expand Up @@ -234,3 +236,5 @@ extension HTTPFields {
self = try parsedFields.trailerFields
}
}

#endif
2 changes: 1 addition & 1 deletion Sources/HTTPTypes/HTTPRequest+URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if FoundationURL
#if FoundationURL && !hasFeature(Embedded)

#if canImport(FoundationEssentials)
public import FoundationEssentials
Expand Down
4 changes: 4 additions & 0 deletions Sources/HTTPTypes/HTTPRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ extension HTTPRequest: CustomDebugStringConvertible {
}
}

#if !hasFeature(Embedded)

extension HTTPRequest.PseudoHeaderFields: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
Expand Down Expand Up @@ -450,6 +452,8 @@ extension HTTPRequest: Codable {
}
}

#endif

extension HTTPRequest.Method {
/// GET
///
Expand Down
4 changes: 4 additions & 0 deletions Sources/HTTPTypes/HTTPResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ extension HTTPResponse: CustomDebugStringConvertible {
}
}

#if !hasFeature(Embedded)

extension HTTPResponse.PseudoHeaderFields: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
Expand Down Expand Up @@ -338,6 +340,8 @@ extension HTTPResponse: Codable {
}
}

#endif

extension HTTPResponse.Status {
// MARK: 1xx

Expand Down
22 changes: 15 additions & 7 deletions Sources/HTTPTypes/ISOLatin1String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ struct ISOLatin1String: Sendable, Hashable {
return string
}

private func withISOLatin1BytesSlowPath<Result>(
_ body: (UnsafeBufferPointer<UInt8>) throws -> Result
) rethrows -> Result {
private func withISOLatin1BytesSlowPath<Return, Failure: Error>(
_ body: (UnsafeBufferPointer<UInt8>) throws(Failure) -> Return
) throws(Failure) -> Return {
try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: self._storage.unicodeScalars.count) { buffer in
for (index, scalar) in self._storage.unicodeScalars.enumerated() {
assert(scalar.value <= UInt8.max)
buffer[index] = UInt8(truncatingIfNeeded: scalar.value)
}
return try body(UnsafeBufferPointer(buffer))
}
return Result { () throws(Failure) in
try body(UnsafeBufferPointer(buffer))
}
}.get()
}

init(_ string: String) {
Expand Down Expand Up @@ -71,10 +73,16 @@ struct ISOLatin1String: Sendable, Hashable {
}
}

func withUnsafeBytes<Result>(_ body: (UnsafeBufferPointer<UInt8>) throws -> Result) rethrows -> Result {
func withUnsafeBytes<Return, Failure: Error>(
_ body: (UnsafeBufferPointer<UInt8>) throws(Failure) -> Return
) throws(Failure) -> Return {
if self._storage.isASCII {
var string = self._storage
return try string.withUTF8(body)
return try string.withUTF8 { buffer in
Result { () throws(Failure) in
try body(buffer)
}
}.get()
} else {
return try self.withISOLatin1BytesSlowPath(body)
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/HTTPTypes/NIOLock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,14 @@ final class LockStorage<Value>: ManagedBuffer<Value, LockPrimitive> {
}
}

func withLockPrimitive<T>(_ body: (UnsafeMutablePointer<LockPrimitive>) throws -> T) rethrows -> T {
try self.withUnsafeMutablePointerToElements { lockPtr in
func withLockPrimitive<T, E: Error>(_ body: (UnsafeMutablePointer<LockPrimitive>) throws(E) -> T) throws(E) -> T {
try self.withUnsafeMutablePointerToElements { lockPtr throws(E) in
try body(lockPtr)
}
}

func withLockedValue<T>(_ mutate: (inout Value) throws -> T) rethrows -> T {
try self.withUnsafeMutablePointers { valuePtr, lockPtr in
func withLockedValue<T, E: Error>(_ mutate: (inout Value) throws(E) -> T) throws(E) -> T {
try self.withUnsafeMutablePointers { valuePtr, lockPtr throws(E) in
LockOperations.lock(lockPtr)
defer { LockOperations.unlock(lockPtr) }
return try mutate(&valuePtr.pointee)
Expand Down
Loading