At present, the company uses the third-party open source library Alamofire for the network request of Swift project. In the process of using, there are two parameter formats that cannot be correctly passed to the back end.

1) The argument contains an empty array

It will be filtered out directly

2) The parameter contains a two-dimensional array

It turns a two-dimensional array into a one-dimensional array

The following will combine the source code of Alamofire parameter to analyze why these two parameter formats are not satisfied step by step.

First, look at the actual results

It’s worth mentioning the benefits of using HttpBin.org for testing the test interface; This is because it returns all parameters received by the server after being called; In this case, we only debug the parameters, so it is easier to see the results of print than using the packet capture tool.

1) Initiate a network request

/ / GET request

AF.request("http://httpbin.org/get", method: .get, parameters: ["name": "kang"."score": 90."nulllArr": []."twoDArr": [["1"."2"], ["3"."4"]]]).responseJSON { (response) in
    switch response.result {
    case .success(let value):
        print(value)
    case .failure(let error):
        print(error)
    }
}
Copy the code

Output results:

{
    args =     {
        name = kang;
        score = 90;
        "twoDArr[][]" =         (
            1.2.3.4
        );
    };
    headers =     {
        Accept = "* / *";
        "Accept-Encoding" = "br; Q = 1.0, gzip; Q = 0.9, deflate; Q = 0.8";
        "Accept-Language" = "en; Q = 1.0";
        Host = "httpbin.org";
        "User-Agent" = "Test / 1.0 (build: 1; IOS 14.0.0) Alamofire / 5.2.2. "";
        "X-Amzn-Trace-Id" = "Root=1-5f8d57cd-039301d94fe8ad1d6311287c";
    };
    origin = "";
    url = "http://httpbin.org/get?name=kang&score=90&twoDArr[][]=1&twoDArr[][]=2&twoDArr[][]=3&twoDArr[][]=4";
}
Copy the code

/ / POST request

AF.request("http://httpbin.org/post", method: .post, parameters: ["name": "kang"."score": 90."nulllArr": []."twoDArr": [["1"."2"], ["3"."4"]]]).responseJSON { (response) in
    switch response.result {
    case .success(let value):
        print(value)
    case .failure(let error):
        print(error)
    }
}
Copy the code

Output results:

{
    args ={}; data= "";
    files ={}; form=     {
        name = kang;
        score = 90;
        "twoDArr[][]" =         (
            1.2.3.4
        );
    };
    headers =     {
        Accept = "* / *";
        "Accept-Encoding" = "br; Q = 1.0, gzip; Q = 0.9, deflate; Q = 0.8";
        "Accept-Language" = "en; Q = 1.0";
        "Content-Length" = 106;
        "Content-Type" = "application/x-www-form-urlencoded; charset=utf-8";
        Host = "httpbin.org";
        "User-Agent" = "The Test / 1.0 (; build:1; IOS 14.0.0) Alamofire / 5.2.2. "";
        "X-Amzn-Trace-Id" = "Root=1-5f8e5cd1-5aa70b642cee70930c0eedd7";
    };
    json = "<null>";
    origin = "";
    url = "https://httpbin.org/post";
}
Copy the code

You can see whether it’s GET or POST, the empty array is filtered out, and the two-dimensional array becomes a one-dimensional array

2) Verify by the packet capture tool Charles

// GET Requests packet capture

// POST requests packet capture

You can see that the parameters of the request and the parameters returned by the interface are the same as those of print above: the nullArr is gone, and the twoDArr is converted to a one-dimensional array. This is different from our original intention of defining the parameter format, and we need to communicate with colleagues in the background to redefine the parameter format.

2, source debugging analysis

The host project currently manages third-party dependency libraries through Pod, and Xcode12 already supports debugging breakpoints between different projects in the same workspace.

Instead of looking at the previous func call, let’s look directly at the argument processing:

ParameterEncoding.swift -> URLEncoding
// MARK: Encoding

public func encode(_ urlRequest: URLRequestConvertible.with parameters: Parameters?). throws -> URLRequest {
    var urlRequest = try urlRequest.asURLRequest()

    guard let parameters = parameters else { return urlRequest }
    
    // Depending on the request method, determine whether the processing parameter is placed after the URL or the body
    if let method = urlRequest.method, destination.encodesParametersInURL(for: method) {
        guard let url = urlRequest.url else {
            throw AFError.parameterEncodingFailed(reason: .missingURL)
        }

        if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
            let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
            urlComponents.percentEncodedQuery = percentEncodedQuery
            urlRequest.url = urlComponents.url
        }
    } else {
        if urlRequest.value(forHTTPHeaderField: "Content-Type") = = nil {
            urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
        }

        urlRequest.httpBody = Data(query(parameters).utf8)
    }

    return urlRequest
}
Copy the code

As you can see from the above source code, there is a call to query(parameters) processing parameter for either request

The core code for handling parameters is the following three methods

private func query(_ parameters: [String: Any]) -> String {
    // Create an array with parameters of type tuple of two strings to store key and value, respectively
    var components: [(String.String)] = []

    // Sort the parameter keys in alphabetical order, and then iterate over each key
    for key in parameters.keys.sorted(by: <) {
        let value = parameters[key]!
        // The parameters are processed according to the value type
        components + = queryComponents(fromKey: key, value: value)
    }
    // Return a parameter string in the format key1=value1&key2=value2
    return components.map { "\ [$0)=\ [The $1)" }.joined(separator: "&")}Copy the code
// Iterate through each layer of the parameter recursively, expanding it to one dimension
public func queryComponents(fromKey key: String.value: Any)- > [(String.String)] {
    // Create an array with parameters of type tuple of two strings to store key and value, respectively
    var components: [(String.String)] = []
    // The value type in the parameter dictionary is dictionary, array, integer, and Boolean respectively
    switch value {
    case let dictionary as [String: Any] :for (nestedKey, value) in dictionary {
            components + = queryComponents(fromKey: "\(key)[\(nestedKey)]. "", value: value)
        }
    case let array as [Any] :for value in array {
            components + = queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
        }
    case let number as NSNumber:
        if number.isBool {
            components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue))))
        } else {
            components.append((escape(key), escape("\(number)")))}case let bool as Bool:
        components.append((escape(key), escape(boolEncoding.encode(value: bool))))
    default:
        components.append((escape(key), escape("\(value)")))}return components
}
Copy the code
// Encode the URL for special characters
public func escape(_ string: String) -> String {
    string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
}
Copy the code
1) For our test parameters, the null array “nulllArr”: []

QueryComponents () {var Components () {var Components () {var Components () {var Components () {var Components () {var Components (); [(String, String)] = [] to components += queryComponents(fromKey: key, value: value);

Here we can look at the += method defined by Array:

@frozen public struct Array<Element> {
   @inlinable public static func + = (lhs: inout [Element].rhs: [Element])
}
Copy the code

Stdlib –public–core–Array. Swift

extension Array {
  @inlinable
  public static func + = (lhs: inout Array.rhs: Array) {
    lhs.append(contentsOf: rhs)
  }
}

@inlinable
  @_semantics("array.append_element")
  public mutating func append(_ newElement: __owned Element) {
    _makeUniqueAndReserveCapacityIfNotUnique()
    let oldCount = _getCount()
    _reserveCapacityAssumingUniqueBuffer(oldCount: oldCount)
    _appendElementAssumeUniqueAndCapacity(oldCount, newElement: newElement)
  }
Copy the code

The array buffer is added when the array to the right of RHS has elements. This is a bit of a stretch, but if you are interested, you can take a closer look at the implementation of this method.

2) For two-dimensional arrays: “twoDArr”: [[“1″,”2”],[“3″,”4”]]

Since it is two-dimensional, the queryComponents method is called recursively to expand all the elements into a one-dimensional array, “twoDArr [] [] “: [“1″,”2″,”3″,”4”], for the key with two brackets, this can be controlled by the code below Alamofire, which is parentheses by default

public init(destination: Destination = .methodDependent,
            arrayEncoding: ArrayEncoding = .brackets,
            boolEncoding: BoolEncoding = .numeric) {
    self.destination = destination
    self.arrayEncoding = arrayEncoding
    self.boolEncoding = boolEncoding
}

public enum ArrayEncoding {
    case brackets
    case noBrackets

    func encode(key: String) -> String {
        switch self {
        case .brackets:
            return "\(key)[]"
        case .noBrackets:
            return key
        }
    }
}
Copy the code

Since we have a two-dimensional array here, the method queryComponents is called recursively twice, so there are two brackets, which also indirectly indicates that the parameter is a two-dimensional array parameter.

conclusion

From the actual parameter request results to step by step analysis of the third-party library source code, we found the specific cause of this result. Alamofire parameter processing includes dictionary, array, integer type, Boolean, etc. In this paper, only empty array and two-dimensional array are verified. The principle of other types is the same, and it can be concluded that Alamofire is used to initiate network requests:

1) If there is an empty dictionary or an empty array in the parameter, it will be filtered out.
2) If there are nested dictionaries and arrays, they will be expanded to one dimension;
3) The key parameter is sorted in positive alphabetic order. If some interfaces have requirements on the parameter order, you can add a positive alphabetic order before the key.

On Github, Alamofire/ Issues also had a problem with the Encodable dictionary instead of the Parameters dictionary. You can see the source code ParameterEncoder. Swift to debug.