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 thisbody
and transform the properties.
Universal body
In the HTTP profile section, we learned that a request body is the original binary data, but,Web API
When 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 asJSON
When we want to have a way to automatically specifyContent-Type: application/json
header 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 haveHTTPBody
Implementation 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 oneData
Value encapsulation intoHTTPBody
inside:
let otherData: Data = ... var request = HTTPRequest() = DataBody(otherData)
JSON Body JSONBody
When sending a network request, encode the value asJSON
It is a very common task. Make oneHTTPBody
It'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-emptyJSON
value. therefore,isEmpty = false
。
Next, most servers are receivingJSON
Need for textapplication/json
ofContent-Type
, so we assume that this is a common situation and inadditionalHeaders
This 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 customizationJSONEncoder
so that customers have the opportunity to fiddle with such.keyEncodingStrategy
Something 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 passesJSONEncoder
Execute 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-Type
Header.
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 roughURL
Encode key-value pairs, e.g.name=Arthur&age=42
。
We will be fromHTTPBody
Start 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-Type
header 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, omittedURLQueryItem
The 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,Web
Servers 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 describeHTTP
Request to load the abstraction layer and use itURLSession
Implement 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!