SoFunction
Updated on 2025-04-11

Detailed explanation of the usage example of HTTP request body Request Bodies in Swift

text

Before sending HTTPRequest request, we slightly improve our structure, and finally, we will output the following information:

public struct HTTPRequest {
    private var urlComponents = URLComponents()
    public var method: HTTPMethod = .get
    public var headers: [String: String] = [:]
    public var body: Data?
}

In this section, we will focus on thisbodyand transform the properties.

Universal body

In the HTTP profile section, we learned that a request body is the original binary data, but,Web APIWhen communicating, this data comes in a variety of standard formats, such as JSON and form submission.

Instead of requiring the client of this code to manually construct a binary representation of its submitted data, we can summarize it as a form of "things that give us data".

Since we do not intend to impose any restrictions on the algorithm used to construct data, it makes sense to define this functionality by protocol rather than concrete types:

public protocol HTTPBody { }

Next, we need a way to get from one of the valuesData, and selectively report errors when problems occur:

public protocol HTTPBody { 
    func encode() throws -> Data 
}

We can stop at this point, but there are two other pieces of information worth having:

public protocol HTTPBody { 
    var isEmpty: Bool { get }
    var additionalHeaders: [String: String] { get } 
    func encode() throws -> Data 
}

If we can quickly know that a body is empty, then we can save the hassle of trying to retrieve any encoded data and handling errors or empty data values.

Additionally, certain types of body text are used in conjunction with the header in the request. For example, when we encode the value asJSONWhen we want to have a way to automatically specifyContent-Type: application/jsonheader without manually specifying it in the request. To do this, we will allow these types to declare additional headers, and these headers will end as part of the final request. To further simplify adoption, we can provide default implementations for these:

extension HTTPBody {
    public var isEmpty: Bool { return false }
    public var additionalHeaders: [String: String] { return [:] }
}

Finally, we can update our type to this new protocol

public struct HTTPRequest {
    private var urlComponents = URLComponents()
    public var method: HTTPMethod = .get
    public var headers: [String: String] = [:]
    public var body: HTTPBody?
}

EmptyBody

The simplest HTTPBody is "bodyless". With this protocol, it is also very convenient to define an empty request body.

public struct EmptyBody: HTTPBody {
    public let isEmpty = true
    public init() { }
    public func encode() throws -> Data { Data() }
}

We can even set it to the default body value, completely eliminating the need for optionality for this property:

public struct HTTPRequest {
    private var urlComponents = URLComponents()
    public var method: HTTPMethod = .get
    public var headers: [String: String] = [:]
    public var body: HTTPBody = EmptyBody()
}

DataBody

The next apparent body type to be implemented is the body that returns any Data value given. This will be used for us not necessarily haveHTTPBodyImplementation But maybe we already have the case where the Data value itself is to be sent.

The specific implementation is as follows:

public struct DataBody: HTTPBody {    
    private let data: Data
    public var isEmpty: Bool {  }
    public var additionalHeaders: [String: String]
    public init(_ data: Data, additionalHeaders: [String: String] = [:]) {
         = data
         = additionalHeaders
    }
    public func encode() throws -> Data { data }    
}

With this, we can easily transfer oneDataValue encapsulation intoHTTPBodyinside:

let otherData: Data = ...
var request = HTTPRequest()
 = DataBody(otherData)

JSON Body JSONBody

When sending a network request, encode the value asJSONIt is a very common task. Make oneHTTPBodyIt's easy to deal with this for us now:

public struct JSONBody: HTTPBody {
    public let isEmpty: Bool = false
    public var additionalHeaders = [
        "Content-Type": "application/json; charset=utf-8"
    ]
    private let encode: () throws -> Data
    public init<T: Encodable>(_ value: T, encoder: JSONEncoder = JSONEncoder()) {
         = { try (value) }
    }
    public func encode() throws -> Data { return try encode() }
}

First, we assume that any value we get will produce at least some results, because even an empty string will be encoded as non-emptyJSONvalue. therefore,isEmpty = false

Next, most servers are receivingJSONNeed for textapplication/jsonofContent-Type, so we assume that this is a common situation and inadditionalHeadersThis value is defaulted in the default value. However, we will keep the property asvar, just in case the customer does not want this.

For encoding, we need to accept some common values ​​(thing to encode), but it is better not to make the entire structure common to the encoding type. We can avoid generic parameters of the type by limiting the generic parameters of the type to the initializer and then capture the generic values ​​in the closure.

We also need a way to provide customizationJSONEncoderso that customers have the opportunity to fiddle with such.keyEncodingStrategySomething like that. However, we will provide a default encoder to simplify usage.

at last,encode()The method itself just calls the closure we created, which captures the common value and passesJSONEncoderExecute it.

One of them is used as follows:

struct PagingParameters: Encodable {
    let page: Int
    let number: Int
}
let parameters = PagingParameters(page: 0, number: 10)
var request = HTTPRequest()
 = JSONBody(parameters)

This way, the body will be automatically encoded as {"page":0,"number":10} and our final request will have the correct oneContent-TypeHeader.

FormBody

The last body we will see in this article is the body representing the basic form submission. When we specifically discuss multi-part form uploads, we will save the file upload for future use.

The form submission text is ultimately roughURLEncode key-value pairs, e.g.name=Arthur&age=42

We will be fromHTTPBodyStart by implementing the same basic structure:

public struct FormBody: HTTPBody {
    public var isEmpty: Bool {  }
    public let additionalHeaders = [
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
    ]
    private let values: [URLQueryItem]
    public init(_ values: [URLQueryItem]) {
         = values
    }
    public init(_ values: [String: String]) {
        let queryItems =  { URLQueryItem(name: $, value: $) }
        (queryItems)
    }
    public func encode() throws -> Data {
        let pieces =  { /* TODO */ }
        let bodyString = (separator: "&")
        return Data(bodyString.utf8)
    }
}

As before, we have a custom oneContent-Typeheader to apply to request. We also expose several initializers so that clients can describe these values ​​in a way that makes sense to them. We also deleted mostencode()Method, omittedURLQueryItemThe actual encoding of the value.

Unfortunately, encoding names and values ​​is a bit ambiguous. If you read the old norms about form submissions you will see mentions of "line wrap normalization" and encoding spaces as+content. We can work hard to dig and find out what these things mean, but in practice,WebServers tend to handle any percentage-encoded content well, even spaces. We will take shortcuts and assume that this is true. We will also fully assume that alphanumeric characters are OK in names and values, and that everything else should be encoded:

private func urlEncode(_ string: String) -> String {
    let allowedCharacters = 
    return (withAllowedCharacters: allowedCharacters) ?? ""
}

use=Character combination name and value:

private func urlEncode(_ queryItem: URLQueryItem) -> String {
    let name = urlEncode()
    let value = urlEncode( ?? "")
    return "(name)=(value)"
}

With this, we can solve it/* TODO */Comment:

public struct FormBody: HTTPBody {
    public var isEmpty: Bool {  }
    public let additionalHeaders = [
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
    ]
    private let values: [URLQueryItem]
    public init(_ values: [URLQueryItem]) {
         = values
    }
    public init(_ values: [String: String]) {
        let queryItems =  { URLQueryItem(name: $, value: $) }
        (queryItems)
    }
    public func encode() throws -> Data {
        let pieces = ()
        let bodyString = (separator: "&")
        return Data(bodyString.utf8)
    }
    private func urlEncode(_ queryItem: URLQueryItem) -> String {
        let name = urlEncode()
        let value = urlEncode( ?? "")
        return "(name)=(value)"
    }
    private func urlEncode(_ string: String) -> String {
        let allowedCharacters = 
        return (withAllowedCharacters: allowedCharacters) ?? ""
    }
}

As before, using it becomes easy:

var request = HTTPRequest()
 = FormBody(["greeting": "Hello, ", "target": "🌎"])
// the body is encoded as:
// greeting=Hello%2C%20&target=%F0%9F%8C%8E

Other Body Other Bodies

There are a variety of body formats that you can send in HTTP requests. I've mentioned that we'll look at multipart requests more carefully in the future, but this HTTPBody method works for almost every request body you'll encounter.

In the next article, we will describeHTTPRequest to load the abstraction layer and use itURLSessionImplement it.

The above is the detailed explanation of the usage example of the HTTP request body Request Bodies in Swift. For more information about the Swift HTTP request body Request Bodies, please follow my other related articles!