Moya: How do I create a path with a '?' within the string?

Created on 12 Jun 2017  路  25Comments  路  Source: Moya/Moya

Moya Version - 8.0.5

When I create a string for my path like:

"/id/?access_token=ACCESSTOKEN"

The '?' gets converted to '%3F' in the url which is sent and the server can't read this. Is there a way to send the '?' rather than the encoded unicode?

What I want to send as the URL:

https://api.test.co.uk/v1-dev/test/3?access_token=e02fkjhbjb4faf363jn0bcbeca0fad4aff

What is actually sent as the URL:

Moya_Logger: [12/06/2017 11:31:20] Request: https://api.test.co.uk/v1-dev/test/3%3Faccess_token=e02fkjhbjb4faf363jn0bcbeca0fad4aff
question

Most helpful comment

@pete183 Yes, URLEncoding.default will put your parameters in the url :wink:

All 25 comments

What's your parameterEncoding?

Just use params instead of embedding em on your own

@BasThomas The parameterEncoding is

URLEncoding.default

@haritowa The server needs the access token and limits to be in the url. Would using params allow for this?

@pete183 Yes, URLEncoding.default will put your parameters in the url :wink:

@pedrovereza @haritowa Thanks for the help, it works now!

Just to add to it: URLEncoding.default will behave differently in GET and POST requests. In GET method you will, in fact, get parameters in URL, @pete183. In POST on the other hand, you will get parameters in body. You can specify if you want it all the time in the URL or body using different properties of URLEncoding - more on the matter here.

@sunshinejr Please check #1120 馃槄

@sunshinejr Does Moya have an easy way to send two parameters one in the URL and one in the body using a POST request?

We don't have a method exclusively for this behavior, but you can use custom ParameterEncoding, that would decide whether the parameter should be in body/url/header. You can find instructions on creating your own encoding here.

Edit: Now that I think about it, that would be a good addition to our examples. If you, @pete183, or someone else end up doing their own ParameterEncoding, we would love to have it added to our examples directory.

@sunshinejr

I think something like this would work:

public var parameters: [String: Any]? {
    var params:[String: Any] = [:]
    params["query"] = ["access_token":getAccessToken()]
    params["body"] = ["user_name":"Pete"]

    return params
}

public var parameterEncoding: ParameterEncoding {
    return CompositeEncoding()
}


struct CompositeEncoding: ParameterEncoding {

    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        guard let parameters = parameters else {
            return try urlRequest.asURLRequest()
        }

        let queryParameters = (parameters["query"] as? Parameters)
        let queryRequest = try URLEncoding(destination: .queryString).encode(urlRequest, with: queryParameters)

        if let body = parameters["body"] {
            let bodyParameters = (body as! Parameters)
            var bodyRequest = try JSONEncoding().encode(urlRequest, with: bodyParameters)

            bodyRequest.url = queryRequest.url
            return bodyRequest
        } else {
            return queryRequest
        }
    }
}

Yeah, I was thinking about something similar. Unfortunately, this way you have to remember about 2 root keys being kinda instructions for sub dictionaries. We can improve it a little bit making these dictionary keys static in the CompositeEncoding:

struct CompositeEncoding: ParameterEncoding {
    enum Keys {
        static let query = "query" 
        static let httpBody = "httpBody"
    }

    ...
}

public var parameters: [String: Any]? {
    return [CompositeEncoding.Keys.query: ["access_token": getAccessToken()],
            CompositeEncoding.Keys.body: ["user_name": "Pete"]]
}

Would you want to make a PR with this, @pete183? It would require adding this example to docs/Examples and linking it in the docs/Examples/Readme.md.

During the next couple of days I'll have a look into this!

Great, thank you! 馃帀

@sunshinejr

There's an issue when you want to provide an array of parameters in a url like this:

/user/17?access_token=8312961fdgdgfmwe3r4f&fields=account_id,photo

It actually gives you:

/user/17?access_token=8312961fdgdgfmwe3r4f&fields%5B%5D=account_id&fields%5B%5D=photo

Here is the swift code for loading the parameters into the url query:

params[ParamKeys.query] = ["fields": ["account_id", "photo"], "access_token":getAccessToken()]

I am not sure if this is supported. Take a look at the discussion in #597, specifically this comment. Does that work?

@BasThomas

When implementing this url param

["fields": "[\"account_id\", \"photo\"]", "access_token":getAccessToken()]

It gives me this:

/user/17?access_token=2db2sdfnkjnefw334819fa&fields=%5B%22account_id%22%2C%20%22photo%22%5D

Rather than:

/user/17?access_token=2db2sdfnkjnefw334819fa&fields=account_id,photo

@pete183 This is the expected behavior from Alamofire. From URLEncoding docs:

Since there is no published specification for how to encode collection types, the convention of appending [] to the key for array values (foo[]=1&foo[]=2), and appending the key surrounded by square brackets for nested dictionary values (foo[bar]=baz).

In order to achieve the format you want, you'd have to implement your own ParameterEncoding

@pedrovereza
Is it possible to tell URLEncoding to stop encoding a comma?

I've got the url to this at the moment:

/user/7?access_token=60323354sgnsldjnfs85a&fields=account_id%2Cphoto
````

And I need

/user/7?access_token=60323354sgnsldjnfs85a&fields=account_id,photo
````

@pete183 Have you got to solve the issue?

@AkhilDad

In the end I wrote a custom composite encoding. This allows for , to not be encoded. Within Moya, you will then need to choose this custom encoding:

/// parameterEncoding: ParameterEncoding
/// - OAuth: URLEncoding.default
/// - Default: CompositeEncoding
public var parameterEncoding: ParameterEncoding {
    switch self {
        case .OAuth:
            return URLEncoding.default
        default:
            return CompositeEncoding()
    }
}
public struct CompositeEncoding: ParameterEncoding {

    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        guard let parameters = parameters else {
            return try urlRequest.asURLRequest()
        }

        let queryParameters = (parameters[ParamKeys.query] as? Parameters)

        var queryRequest = try URLEncoding(destination: .queryString).encode(urlRequest, with: queryParameters)

        if let body = parameters[ParamKeys.httpBody] {
            let bodyParameters = (body as? Parameters)
            var bodyRequest = try URLEncoding().encode(urlRequest, with: bodyParameters)
            //var bodyRequest = try JSONEncoding().encode(urlRequest, with: bodyParameters)
            bodyRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
            bodyRequest.url = queryRequest.url

            return bodyRequest
        } else {
            return queryRequest
        }
    }
}




// MARK: URLEncoding
/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
/// the HTTP body depends on the destination of the encoding.
///
/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
/// `application/x-www-form-urlencoded; charset=utf-8`. Since there is no published specification for how to encode
/// collection types, the convention of appending `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending
/// the key surrounded by square brackets for nested dictionary values (`foo[bar]=baz`).
public struct URLEncoding: ParameterEncoding {

    // MARK: Helper Types
    /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the
    /// resulting URL request.
    ///
    /// - methodDependent: Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE`
    ///                    requests and sets as the HTTP body for requests with any other HTTP method.
    /// - queryString:     Sets or appends encoded query string result to existing query string.
    /// - httpBody:        Sets encoded query string result as the HTTP body of the URL request.
    public enum Destination {
        case methodDependent, queryString, httpBody
    }

    // MARK: Properties
    /// Returns a default `URLEncoding` instance.
    public static var `default`: URLEncoding { return URLEncoding() }

    /// Returns a `URLEncoding` instance with a `.methodDependent` destination.
    public static var methodDependent: URLEncoding { return URLEncoding() }

    /// Returns a `URLEncoding` instance with a `.queryString` destination.
    public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }

    /// Returns a `URLEncoding` instance with an `.httpBody` destination.
    public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }

    /// The destination defining where the encoded query string is to be applied to the URL request.
    public let destination: Destination

    // MARK: Initialization
    /// Creates a `URLEncoding` instance using the specified destination.
    ///
    /// - parameter destination: The destination defining where the encoded query string is to be applied.
    ///
    /// - returns: The new `URLEncoding` instance.
    public init(destination: Destination = .methodDependent) {
        self.destination = destination
    }

    // MARK: Encoding
    /// Creates a URL request by encoding parameters and applying them onto an existing request.
    ///
    /// - parameter urlRequest: The request to have parameters applied.
    /// - parameter parameters: The parameters to apply.
    ///
    /// - throws: An `Error` if the encoding process encounters an error.
    ///
    /// - returns: The encoded request.
    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        var urlRequest = try urlRequest.asURLRequest()

        guard let parameters = parameters else { return urlRequest }

        if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: 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 = query(parameters).data(using: .utf8, allowLossyConversion: false)
        }

        return urlRequest
    }

    /// Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion.
    ///
    /// - parameter key:   The key of the query component.
    /// - parameter value: The value of the query component.
    ///
    /// - returns: The percent-escaped, URL encoded query string components.
    public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
        var components: [(String, String)] = []

        if let dictionary = value as? [String: Any] {
            for (nestedKey, value) in dictionary {
                components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
            }
        } else if let array = value as? [Any] {
            for value in array {
                components += queryComponents(fromKey: "\(key)[]", value: value)
            }
        } else if let value = value as? NSNumber {
            if value.isBool {
                components.append((escape(key), escape((value.boolValue ? "1" : "0"))))
            } else {
                components.append((escape(key), escape("\(value)")))
            }
        } else if let bool = value as? Bool {
            components.append((escape(key), escape((bool ? "1" : "0"))))
        } else {
            components.append((escape(key), escape("\(value)")))
        }

        return components
    }

    /// Returns a percent-escaped string following RFC 3986 for a query string key or value.
    ///
    /// RFC 3986 states that the following characters are "reserved" characters.
    ///
    /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
    /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
    ///
    /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
    /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
    /// should be percent-escaped in the query string.
    ///
    /// - parameter string: The string to be percent-escaped.
    ///
    /// - returns: The percent-escaped string.
    public func escape(_ string: String) -> String {
        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = "!$&'()*+;="

        var allowedCharacterSet = CharacterSet.urlQueryAllowed
        allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")

        var escaped = ""

        //==========================================================================================================
        //
        //  Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
        //  hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
        //  longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
        //  info, please refer to:
        //
        //      - https://github.com/Alamofire/Alamofire/issues/206
        //
        //==========================================================================================================
        if #available(iOS 8.3, *) {
            escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
        } else {
            let batchSize = 50
            var index = string.startIndex

            while index != string.endIndex {
                let startIndex = index
                let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
                let range = startIndex..<endIndex

                let substring = string.substring(with: range)

                escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? substring

                index = endIndex
            }
        }

        return escaped
    }

    private func query(_ parameters: [String: Any]) -> String {
        var components: [(String, String)] = []

        for key in parameters.keys.sorted(by: <) {
            let value = parameters[key]!
            components += queryComponents(fromKey: key, value: value)
        }
        #if swift(>=4.0)
            return components.map { "\($0.0)=\($0.1)" }.joined(separator: "&")
        #else
            return components.map { "\($0)=\($1)" }.joined(separator: "&")
        #endif
    }

    private func encodesParametersInURL(with method: HTTPMethod) -> Bool {
        switch destination {
        case .queryString:
            return true
        case .httpBody:
            return false
        default:
            break
        }

        switch method {
        case .get, .head, .delete:
            return true
        default:
            return false
        }
    }
}



extension NSNumber {
    fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) }
}

@pete183 I don't want to add params, I want to send it as path, as this is the next url coming in my pagination which I can append after my base path

@AkhilDad This is to send it as a path, it's just that in Alamofire, they call it parameter encoding. If you give me a sample of the path which you would like to send data too, I could create it.

@pete183 So the problem was in headers I was getting next and previous urls sub path which I should append after base path
ex http://www.basepath.com/apis/v1 is base path
and next and previous path comes like questions/?page=2&topic=xyz
Now I need to append this after base url so I was trying to append this path /v3/questions/?page=2&topic=xyz but due to encoding I ended up with unwanted encoded characters for = and others

I have finally solved it using

public var baseURL: URL {
        switch self {
        case .whenPath(let nextUrl):
            return URL(string: self.appSettingsService.networkConfig.baseUrl+nextUrl)!
         default:
            return URL(string: self.appSettingsService.networkConfig.baseUrl)!
        }
    }

I want to call this https://www.blaalb.com/restApi/testApi/myOrders?number=1&month=24

enum MyWebService {
    switch self {
       case getMyOrders(number: Int, month: Int)
   }
}

var path: String {
   switch self {
      case . getMyOrders:
      return "/myOrders"
     }
  }
}

var method: Moya.Method {
     switch self {
      case .getMyOrders:
      return .get
   }
}

var task: Task {
    switch self {
      case . getMyOrders(let number, let month):
      let p = ["number" : number,
               "month": month]
    return .requestParameters(parameters:p , encoding: URLEncoding.default)
 }
}

Just to add to it: URLEncoding.default will behave differently in GET and POST requests. In GET method you will, in fact, get parameters in URL, @pete183. In POST on the other hand, you will get parameters in body. You can specify if you want it all the time in the URL or body using different properties of URLEncoding - more on the matter here.

Thanks I was looking for this solution since my POST request was not sending params in URL. changed encoding to URLEncoding.queryString and all good now.

Was this page helpful?
0 / 5 - 0 ratings