17

I'm using NSURLComponents and I can't seem to get the query values to encode correctly. I need the final URL to represent a + as %2B.

let baseUrl = NSURL(string: "http://www.example.com")    
let components = NSURLComponents(URL: baseUrl, resolvingAgainstBaseURL: true)
components.queryItems = [ NSURLQueryItem(name: "name", value: "abc+def") ]
XCTAssertEqual(components!.string!, "http://www.example.com?connectionToken=abc%2Bdef")

Failed!

Output equals:

http://www.example.com?connectionToken=abc+def 

NOT

http://www.example.com?connectionToken=abc%2Bdef

I've tried several variations and I just can't seem to get it to output %2B at all.

Rory McCrossan
  • 331,213
  • 40
  • 305
  • 339
Col
  • 2,300
  • 3
  • 16
  • 16

4 Answers4

27

My answer from Radar 24076063 with an explanation of why it works the way it does (with a little cleanup of the text):

The '+' character is legal in the query component so it does not need to be percent-encoded.

Some systems use the '+' as a space and require '+' the plus character to be percent-encoded. However, that kind of two stage encoding (converting plus sign to %2B and then converting space to plus sign) is prone to errors because it easily leads to encoding problems. It also breaks if the URL is normalized (syntax normalization of URLs includes the removal of all unnecessary percent-encoding — see rfc3986 section 6.2.2.2).

So, if you need that behavior because of the server your code is talking to, you'll handle the extra transformation(s) yourself. Here's a snippet of code that shows what you need to do both ways:

NSURLComponents *components = [[NSURLComponents alloc] init];
NSArray *items = [NSArray arrayWithObjects:[NSURLQueryItem queryItemWithName:@"name" value:@"Value +"], nil];
components.queryItems = items;
NSLog(@"URL queryItems: %@", [components queryItems]);
NSLog(@"URL string before: %@", [components string]);

// Replace all "+" in the percentEncodedQuery with "%2B" (a percent-encoded +) and then replace all "%20" (a percent-encoded space) with "+"
components.percentEncodedQuery = [[components.percentEncodedQuery stringByReplacingOccurrencesOfString:@"+" withString:@"%2B"] stringByReplacingOccurrencesOfString:@"%20" withString:@"+"];
NSLog(@"URL string after: %@", [components string]);

// This is the reverse if you receive a URL with a query in that form and want to parse it with queryItems
components.percentEncodedQuery = [[components.percentEncodedQuery stringByReplacingOccurrencesOfString:@"+" withString:@"%20"] stringByReplacingOccurrencesOfString:@"%2B" withString:@"+"];
NSLog(@"URL string back: %@", [components string]);
NSLog(@"URL queryItems: %@", [components queryItems]);

The output is:

URL queryItems: (
    "<NSURLQueryItem 0x100502460> {name = name, value = Value +}"
)

URL string before: ?name=Value%20+

URL string after: ?name=Value+%2B

URL string back: ?name=Value%20+

URL queryItems: (
    "<NSURLQueryItem 0x1002073e0> {name = name, value = Value +}"
)
Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38
Jim Luther
  • 425
  • 4
  • 10
  • 2
    Moral: When adding API for which there is no solid spec, you have to make decisions on the fuzzy parts and those decision won't work for everyone. With NSURLComponents.queryItems, we decided to follow rfc3986/std66 (the URI standard) for what characters should and should not be percent-encoded in the name and value strings. The only valid query component characters that are percent-encoded in the name and value strings are the query item delimiters '&' and '='. If you want other characters replaced or percent-encoded, the code snippets here show how that is done. – Jim Luther May 20 '16 at 00:47
  • In case it's not clear: "we" in the above comment by Jim means the devs at that company you bought your computer from ;) – Thomas Tempelmann May 23 '16 at 15:24
  • The problem is, Oracle's Java URL encoding uses/interprets '+' encoding. Because NSURLComponents doesn't encode the '+' sign, we have to translate '+' in different ways, depending on whether the URL is outgoing or being constructed. If NSURLComponents just encoded the '+' sign, the compatibility problem would become trivial — just convert '+' to %20 and things would always work. :-( – dgatwood Aug 27 '18 at 17:57
  • If you're using macOS 10.13/iOS 11.0 or later, a new NSURLComponents property was added: percentEncodedQueryItems. percentEncodedQueryItems lets your code deal with the percent-encoding and so you can choose to encode additional character which are not normally percent-encoded. The property is well-documented in the NSURL.h header file. – Jim Luther Jan 22 '22 at 18:23
11

As the other answers mention, "+" isn't encoded on iOS by default. But if your server requires that to be encoded, here's how to do it:

var comps = URLComponents(url: self, resolvingAgainstBaseURL: true)
// a local var is needed to fix a swift warning about "overlapping accesses" caused by writing to the same property that's being read.
var compsCopy = comps
compsCopy?.queryItems = [URLQueryItem(name: "name", value: "abc+def")]
comps?.percentEncodedQuery = compsCopy?.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
Victor Bogdan
  • 2,022
  • 24
  • 33
4

+ may be a valid character when the content-type is application/x-www-form-urlencoded, see the link, so NSURLComponents doesn't encode it.

Apple also mention this:

RFC 3986 specifies which characters must be percent-encoded in the query component of a URL, but not how those characters should be interpreted. The use of delimited key-value pairs is a common convention, but isn't standardized by a specification. Therefore, you may encounter interoperability problems with other implementations that follow this convention.

One notable example of potential interoperability problems is how the plus sign (+) character is handled:

According to RFC 3986, the plus sign is a valid character within a query, and doesn't need to be percent-encoded. However, according to the W3C recommendations for URI addressing, the plus sign is reserved as shorthand notation for a space within a query string (for example, ?greeting=hello+world).

If a URL query component contains a date formatted according to RFC 3339 with a plus sign in the timezone offset (for example, 2013-12-31T14:00:00+00:00), interpreting the plus sign as a space results in an invalid time format. RFC 3339 specifies how dates should be formatted, but doesn't advise whether the plus sign must be percent-encoded in a URL. Depending on the implementation receiving this URL, you may need to preemptively percent-encode the plus sign character.

As an alternative, consider encoding complex and/or potentially problematic data in a more robust data-interchange format, such as JSON or XML.

The conclude is you may or may not encode '+'.

In my opinion, NSURLComponents only encode character which it make sure that should be encoded, such as '&', '=' or Chinese characters like '你' '好', it doesn't encode the character may be encode or not according to the content-type, like '+' I mentioned above. So if you find you have to encode '+' or your server can't parse correctly, you can use the code below.

I don't know swift, so I just provide objective-c code, sorry for that.

- (NSString *)URLEncodingValue:(NSString *)value
{
    NSCharacterSet *set = [NSCharacterSet URLQueryAllowedCharacterSet];
    NSMutableCharacterSet *mutableQueryAllowedCharacterSet = [set mutableCopy];
    [mutableQueryAllowedCharacterSet removeCharactersInString:@"!*'();:@&=+$,/?%#[]"];
    return [value stringByAddingPercentEncodingWithAllowedCharacters:mutableQueryAllowedCharacterSet];
}

!*'();:@&=+$,/?%#[] are reserved characters defined in RFC 3986, the code will encode all of them appear in the value parameter, if you just want to encode '+', just replace !*'();:@&=+$,/?%#[] with +.

Community
  • 1
  • 1
KudoCC
  • 6,912
  • 1
  • 24
  • 53
0

This is an Apple bug. Instead use

NSString -stringByAddingPercentEncodingWithAllowedCharacters:

with

NSCharacterSet +URLQueryAllowedCharacterSet
Reed Morse
  • 1,398
  • 2
  • 10
  • 17
  • 1
    Indeed it's not a bug. The specs are (very much) fuzzy, as always with pretty much anything HTTP-related… – Frizlab Feb 10 '17 at 19:14