Skip to content

Commit

Permalink
Fix broken URI behaviors (#3140)
Browse files Browse the repository at this point in the history
* Fix the issues with URI's behavior and add tests for the various issues reported on GitHub
* Fix Sendable correctness in the various content coder subsystems
* Tweak a few timeouts to reduce test runtime (reduced by 2/3)
* Use app.startup() rather than app.start() in async contexts in tests
* Minor README updates
* Add Mastodon link to replace old Twitter one
* Add missing image alt text
  • Loading branch information
gwynne committed Jan 22, 2024
1 parent 0680f9f commit d5025b3
Show file tree
Hide file tree
Showing 21 changed files with 486 additions and 348 deletions.
25 changes: 14 additions & 11 deletions README.md
Expand Up @@ -5,23 +5,26 @@
</a>

<p align="center">
<a href="https://docs.vapor.codes/4.0/">
<img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation">
<a href="https://docs.vapor.codes/4.0/">
<img src="https://design.vapor.codes/images/readthedocs.svg" alt="Documentation">
</a>
<a href="https://discord.gg/vapor">
<img src="https://img.shields.io/discord/431917998102675485.svg" alt="Team Chat">
<img src="https://design.vapor.codes/images/discordchat.svg" alt="Team Chat">
</a>
<a href="LICENSE">
<img src="https://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
<img src="https://design.vapor.codes/images/mitlicense.svg" alt="MIT License">
</a>
<a href="https://github.com/vapor/vapor/actions">
<img src="https://github.com/vapor/vapor/actions/workflows/test.yml/badge.svg?branch=main" alt="Continuous Integration">
<a href="https://github.com/vapor/vapor/actions/workflows/test.yml">
<img src="https://img.shields.io/github/actions/workflow/status/vapor/vapor/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration">
</a>
<a href="https://codecov.io/gh/vapor/vapor">
<img src="https://img.shields.io/codecov/c/github/vapor/vapor?style=plastic&logo=codecov&label=codecov" alt="Code Coverage">
</a>
<a href="https://swift.org">
<img src="https://img.shields.io/badge/swift-5.7-brightgreen.svg" alt="Swift 5.7">
<img src="https://design.vapor.codes/images/swift57up.svg" alt="Swift 5.7+">
</a>
<a href="https://twitter.com/codevapor">
<img src="https://img.shields.io/badge/twitter-codevapor-5AA9E7.svg" alt="Twitter">
<a href="https://hachyderm.io/@codevapor">
<img src="https://img.shields.io/badge/%20-@codevapor-6364f6.svg?style=plastic&logo=mastodon&labelColor=gray&logoColor=%239394ff" alt="Mastodon">
</a>
</p>

Expand All @@ -33,11 +36,11 @@ Take a look at some of the [awesome stuff](https://github.com/Cellane/awesome-va

### 💧 Community

Join the welcoming community of fellow Vapor developers on [Discord](http://vapor.team).
Join the welcoming community of fellow Vapor developers on [Discord](https://vapor.team).

### 🚀 Contributing

To contribute a **feature or idea** to Vapor, [create an issue](https://github.com/vapor/vapor/issues/new) explaining your idea or bring it up on [Discord](http://vapor.team).
To contribute a **feature or idea** to Vapor, [create an issue](https://github.com/vapor/vapor/issues/new) explaining your idea or bring it up on [Discord](https://vapor.team).

If you find a **bug**, please [create an issue](https://github.com/vapor/vapor/issues/new).

Expand Down
2 changes: 1 addition & 1 deletion Sources/Vapor/Content/ContainerGetPathExecutor.swift
Expand Up @@ -2,7 +2,7 @@
internal struct ContainerGetPathExecutor<D: Decodable>: Decodable {
let result: D

static func userInfo(for keyPath: [CodingKey]) -> [CodingUserInfoKey: Any] {
static func userInfo(for keyPath: [CodingKey]) -> [CodingUserInfoKey: Sendable] {
[.containerGetKeypath: keyPath]
}

Expand Down
8 changes: 4 additions & 4 deletions Sources/Vapor/Content/ContentCoders.swift
Expand Up @@ -21,7 +21,7 @@ public protocol ContentEncoder {
///
/// For legacy API compatibility reasons, the default protocol conformance for this method forwards it to the legacy
/// encode method.
func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws
func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws
where E: Encodable
}

Expand All @@ -42,20 +42,20 @@ public protocol ContentDecoder {
///
/// For legacy API compatibility reasons, the default protocol conformance for this method forwards it to the legacy
/// decode method.
func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D
func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D
where D: Decodable
}

extension ContentEncoder {
public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws
public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws
where E: Encodable
{
try self.encode(encodable, to: &body, headers: &headers)
}
}

extension ContentDecoder {
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D
where D: Decodable
{
try self.decode(decodable, from: body, headers: headers)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Vapor/Content/ContentContainer.swift
Expand Up @@ -131,12 +131,12 @@ extension ContentContainer {

/// Injects coder userInfo into a ``ContentDecoder`` so we don't have to add passthroughs to ``ContentContainer``.
fileprivate struct ForwardingContentDecoder: ContentDecoder {
let base: ContentDecoder, info: [CodingUserInfoKey: Any]
let base: ContentDecoder, info: [CodingUserInfoKey: Sendable]

func decode<D: Decodable>(_: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D {
try self.base.decode(D.self, from: body, headers: headers, userInfo: self.info)
}
func decode<D: Decodable>(_: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D {
func decode<D: Decodable>(_: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D {
try self.base.decode(D.self, from: body, headers: headers, userInfo: userInfo.merging(self.info) { $1 })
}
}
4 changes: 2 additions & 2 deletions Sources/Vapor/Content/JSONCoders+Content.swift
Expand Up @@ -9,7 +9,7 @@ extension JSONEncoder: ContentEncoder {
try self.encode(encodable, to: &body, headers: &headers, userInfo: [:])
}

public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws
public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws
where E: Encodable
{
headers.contentType = .json
Expand All @@ -36,7 +36,7 @@ extension JSONDecoder: ContentDecoder {
try self.decode(D.self, from: body, headers: headers, userInfo: [:])
}

public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D
where D: Decodable
{
let data = body.getData(at: body.readerIndex, length: body.readableBytes) ?? Data()
Expand Down
4 changes: 2 additions & 2 deletions Sources/Vapor/Content/PlaintextDecoder.swift
Expand Up @@ -13,7 +13,7 @@ public struct PlaintextDecoder: ContentDecoder {
}

/// `ContentDecoder` conformance.
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D
where D : Decodable
{
let string = body.getString(at: body.readerIndex, length: body.readableBytes)
Expand All @@ -29,7 +29,7 @@ private final class _PlaintextDecoder: Decoder, SingleValueDecodingContainer {
let userInfo: [CodingUserInfoKey: Any]
let plaintext: String?

init(plaintext: String?, userInfo: [CodingUserInfoKey: Any] = [:]) {
init(plaintext: String?, userInfo: [CodingUserInfoKey: Sendable] = [:]) {
self.plaintext = plaintext
self.userInfo = userInfo
}
Expand Down
9 changes: 5 additions & 4 deletions Sources/Vapor/Content/PlaintextEncoder.swift
Expand Up @@ -26,12 +26,12 @@ public struct PlaintextEncoder: ContentEncoder {
try self.encode(encodable, to: &body, headers: &headers, userInfo: [:])
}

public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws
public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws
where E: Encodable
{
let actualEncoder: _PlaintextEncoder
if !userInfo.isEmpty { // Changing a coder's userInfo is a thread-unsafe mutation, operate on a copy
actualEncoder = _PlaintextEncoder(userInfo: self.encoder.userInfo.merging(userInfo) { $1 })
actualEncoder = _PlaintextEncoder(userInfo: self.encoder.userInfoSendable.merging(userInfo) { $1 })
} else {
actualEncoder = self.encoder
}
Expand All @@ -51,10 +51,11 @@ public struct PlaintextEncoder: ContentEncoder {

private final class _PlaintextEncoder: Encoder, SingleValueEncodingContainer {
public var codingPath: [CodingKey] = []
public var userInfo: [CodingUserInfoKey: Any]
fileprivate var userInfoSendable: [CodingUserInfoKey: Sendable]
public var userInfo: [CodingUserInfoKey: Any] { self.userInfoSendable }
public var plaintext: String?

public init(userInfo: [CodingUserInfoKey: Any] = [:]) { self.userInfo = userInfo }
public init(userInfo: [CodingUserInfoKey: Sendable] = [:]) { self.userInfoSendable = userInfo }

public func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> { .init(FailureEncoder<Key>()) }
public func unkeyedContainer() -> UnkeyedEncodingContainer { FailureEncoder() }
Expand Down
8 changes: 4 additions & 4 deletions Sources/Vapor/Content/URLQueryCoders.swift
Expand Up @@ -2,28 +2,28 @@ public protocol URLQueryDecoder {
func decode<D>(_ decodable: D.Type, from url: URI) throws -> D
where D: Decodable

func decode<D>(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Any]) throws -> D
func decode<D>(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Sendable]) throws -> D
where D: Decodable
}

public protocol URLQueryEncoder {
func encode<E>(_ encodable: E, to url: inout URI) throws
where E: Encodable

func encode<E>(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Any]) throws
func encode<E>(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Sendable]) throws
where E: Encodable
}

extension URLQueryEncoder {
public func encode<E>(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Any]) throws
public func encode<E>(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Sendable]) throws
where E: Encodable
{
try self.encode(encodable, to: &url)
}
}

extension URLQueryDecoder {
public func decode<D>(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Any]) throws -> D
public func decode<D>(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Sendable]) throws -> D
where D: Decodable
{
try self.decode(decodable, from: url)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Vapor/Content/URLQueryContainer.swift
Expand Up @@ -98,10 +98,10 @@ extension URLQueryContainer {

/// Injects coder userInfo into a ``URLQueryDecoder`` so we don't have to add passthroughs to ``URLQueryContainer``.
fileprivate struct ForwardingURLQueryDecoder: URLQueryDecoder {
let base: URLQueryDecoder, info: [CodingUserInfoKey: Any]
let base: URLQueryDecoder, info: [CodingUserInfoKey: Sendable]

func decode<D: Decodable>(_: D.Type, from url: URI) throws -> D { try self.base.decode(D.self, from: url, userInfo: self.info) }
func decode<D: Decodable>(_: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Any]) throws -> D {
func decode<D: Decodable>(_: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Sendable]) throws -> D {
try self.base.decode(D.self, from: url, userInfo: userInfo.merging(self.info) { $1 })
}
}
2 changes: 1 addition & 1 deletion Sources/Vapor/Multipart/FormDataDecoder+Content.swift
Expand Up @@ -9,7 +9,7 @@ extension FormDataDecoder: ContentDecoder {
try self.decode(D.self, from: body, headers: headers, userInfo: [:])
}

public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D
where D: Decodable
{
guard let boundary = headers.contentType?.parameters["boundary"] else {
Expand Down
10 changes: 4 additions & 6 deletions Sources/Vapor/Multipart/FormDataEncoder+Content.swift
Expand Up @@ -3,19 +3,17 @@ import NIOHTTP1
import NIOCore

extension FormDataEncoder: ContentEncoder {
public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
where E: Encodable
{
public func encode<E: Encodable>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws {
try self.encode(encodable, to: &body, headers: &headers, userInfo: [:])
}

public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws
where E: Encodable
{
public func encode<E: Encodable>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws {
let boundary = "----vaporBoundary\(randomBoundaryData())"

headers.contentType = HTTPMediaType(type: "multipart", subType: "form-data", parameters: ["boundary": boundary])
if !userInfo.isEmpty {
var actualEncoder = self // Changing a coder's userInfo is a thread-unsafe mutation, operate on a copy

actualEncoder.userInfo.merge(userInfo) { $1 }
return try actualEncoder.encode(encodable, boundary: boundary, into: &body)
} else {
Expand Down

0 comments on commit d5025b3

Please sign in to comment.