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
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.9
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?

Copy link
Copy Markdown
Member

@MaxDesiatov MaxDesiatov Apr 10, 2026

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)

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
4 changes: 4 additions & 0 deletions Sources/HTTPTypes/HTTPRequest+URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

#if !hasFeature(Embedded)

#if canImport(FoundationEssentials)
import FoundationEssentials
#else // canImport(FoundationEssentials)
Expand Down Expand Up @@ -214,3 +216,5 @@ extension URL {
#endif // !canImport(FoundationEssentials) && canImport(CoreFoundation)
}
}

#endif
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 @@ -176,14 +176,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