From 58952a8bd55238c70302423fce36c155640bc910 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Tue, 16 Dec 2025 23:13:25 +0000 Subject: [PATCH 1/2] `buildRequest` function to avoid duplication while preparing rest requests. --- Source/ARTAuth.m | 27 ++- Source/ARTDataQuery.m | 32 +-- Source/ARTJsonLikeEncoder.m | 2 +- Source/ARTNSString+ARTUtil.m | 14 ++ Source/ARTPushActivationStateMachine.m | 77 ++++-- Source/ARTPushAdmin.m | 16 +- Source/ARTPushChannel.m | 111 ++++++--- Source/ARTPushChannelSubscriptions.m | 76 ++++-- Source/ARTPushDeviceRegistrations.m | 98 ++++++-- Source/ARTRealtimeChannel.m | 13 +- Source/ARTRealtimePresence.m | 1 - Source/ARTRest.m | 145 +++++++---- Source/ARTRestAnnotations.m | 54 +++-- Source/ARTRestChannel.m | 225 +++++++++--------- Source/ARTRestPresence.m | 42 +++- Source/ARTStats.m | 9 +- Source/ARTTypes.m | 15 -- .../Ably/ARTDataQuery+Private.h | 4 +- Source/PrivateHeaders/Ably/ARTEncoder.h | 2 + .../PrivateHeaders/Ably/ARTNSString+ARTUtil.h | 1 + .../PrivateHeaders/Ably/ARTPresence+Private.h | 6 - Source/PrivateHeaders/Ably/ARTRest+Private.h | 8 + Source/include/Ably/ARTTypes.h | 8 +- Test/AblyTests/Tests/RestClientTests.swift | 6 +- 24 files changed, 624 insertions(+), 368 deletions(-) diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 690c2c34b..554b0d277 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -493,21 +493,24 @@ - (void)handleAuthUrlResponse:(NSHTTPURLResponse *)response - (NSObject *)executeTokenRequest:(ARTTokenRequest *)tokenRequest callback:(ARTTokenDetailsCallback)callback { id encoder = _rest.defaultEncoder; - - NSURL *requestUrl = [NSURL URLWithString:[NSString stringWithFormat:@"/keys/%@/requestToken?format=%@", tokenRequest.keyName, [encoder formatAsString]] - relativeToURL:_rest.baseUrl]; + NSError *error = nil; + NSData *bodyData = [encoder encodeTokenRequest:tokenRequest error:&error]; + if (error) { + callback(nil, error); + return nil; + } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl]; - request.HTTPMethod = @"POST"; - - NSError *encodeError = nil; - request.HTTPBody = [encoder encodeTokenRequest:tokenRequest error:&encodeError]; - if (encodeError) { - callback(nil, encodeError); + NSMutableURLRequest *request = [_rest buildRequest:@"POST" + path:[NSString stringWithFormat:@"/keys/%@/requestToken", tokenRequest.keyName] + baseUrl:nil + params:@{ @"format": encoder.formatAsString } + body:bodyData + headers:nil + error:&error]; + if (error) { + callback(nil, error); return nil; } - [request setValue:[encoder mimeType] forHTTPHeaderField:@"Accept"]; - [request setValue:[encoder mimeType] forHTTPHeaderField:@"Content-Type"]; return [_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOff wrapperSDKAgents:nil completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (error) { diff --git a/Source/ARTDataQuery.m b/Source/ARTDataQuery.m index af98d8da8..29d226b02 100755 --- a/Source/ARTDataQuery.m +++ b/Source/ARTDataQuery.m @@ -22,18 +22,18 @@ - (instancetype)init { } } -- (NSMutableArray *)asQueryItems:(NSError *_Nullable*)error { - NSMutableArray *items = [NSMutableArray array]; +- (NSStringDictionary *)asQueryParams { + NSMutableStringDictionary *items = [NSMutableStringDictionary dictionary]; if (self.start) { - [items addObject:[NSURLQueryItem queryItemWithName:@"start" value:[NSString stringWithFormat:@"%llu", dateToMilliseconds(self.start)]]]; + items[@"start"] = [NSString stringWithFormat:@"%llu", dateToMilliseconds(self.start)]; } if (self.end) { - [items addObject:[NSURLQueryItem queryItemWithName:@"end" value:[NSString stringWithFormat:@"%llu", dateToMilliseconds(self.end)]]]; + items[@"end"] = [NSString stringWithFormat:@"%llu", dateToMilliseconds(self.end)]; } - - [items addObject:[NSURLQueryItem queryItemWithName:@"limit" value:[NSString stringWithFormat:@"%hu", self.limit]]]; - [items addObject:[NSURLQueryItem queryItemWithName:@"direction" value:queryDirectionToString(self.direction)]]; + + items[@"limit"] = [NSString stringWithFormat:@"%hu", self.limit]; + items[@"direction"] = queryDirectionToString(self.direction); return items; } @@ -42,20 +42,10 @@ - (NSMutableArray *)asQueryItems:(NSError *_Nullable*)error { @implementation ARTRealtimeHistoryQuery -- (NSMutableArray *)asQueryItems:(NSError **)errorPtr { - NSMutableArray *items = [super asQueryItems:errorPtr]; - if (*errorPtr) { - return nil; - } - if (self.untilAttach) { - NSAssert(self.realtimeChannel, @"ARTRealtimeHistoryQuery used from outside ARTRealtimeChannel.history"); - if (self.realtimeChannel.state_nosync != ARTRealtimeChannelAttached) { - *errorPtr = [NSError errorWithDomain:ARTAblyErrorDomain code:ARTRealtimeHistoryErrorNotAttached userInfo:@{NSLocalizedDescriptionKey:@"ARTRealtimeHistoryQuery: untilAttach used in channel that isn't attached"}]; - return nil; - } - [items addObject:[NSURLQueryItem queryItemWithName:@"fromSerial" value:self.realtimeChannel.attachSerial]]; - } - return items; +- (NSStringDictionary *)asQueryParams { + NSMutableStringDictionary *params = super.asQueryParams.mutableCopy; + params[@"fromSerial"] = self.realtimeChannelAttachSerial; + return params; } @end diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index 69fad01d2..b41c242eb 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -617,7 +617,7 @@ - (ARTAuthDetails *)authDetailsFromDictionary:(NSDictionary *)input { return [[ARTAuthDetails alloc] initWithToken:[input artString:@"accessToken"]]; } -- (NSArray *)messagesToArray:(NSArray *)messages { +- (NSArray *)messagesToArray:(NSArray *)messages { NSMutableArray *output = [NSMutableArray array]; for (ARTMessage *message in messages) { diff --git a/Source/ARTNSString+ARTUtil.m b/Source/ARTNSString+ARTUtil.m index 53686cdf7..8dfe3559a 100644 --- a/Source/ARTNSString+ARTUtil.m +++ b/Source/ARTNSString+ARTUtil.m @@ -17,4 +17,18 @@ - (BOOL)isNotEmptyString { return ![self isEmptyString]; } +- (NSString *)encodePathSegment { + // Source: https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + // i.e. segment = unreserved / pct-encoded / sub-delims / ":" / "@", where + // unreserved = ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~ + // pct-encoded = %XX + // sub-delims = !$&'()*+,;= + NSCharacterSet *allowedSet = [NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&'()*+,;=:@"]; + NSString *escaped = [self stringByAddingPercentEncodingWithAllowedCharacters:allowedSet]; + if (!escaped) { + [NSException raise:NSInternalInconsistencyException format:@"String '%@' can't be percent encoded.", self]; + } + return escaped; +} + @end diff --git a/Source/ARTPushActivationStateMachine.m b/Source/ARTPushActivationStateMachine.m index f3de57bfe..2d13be7fa 100644 --- a/Source/ARTPushActivationStateMachine.m +++ b/Source/ARTPushActivationStateMachine.m @@ -190,11 +190,19 @@ - (void)deviceRegistration:(ARTErrorInfo *)error { } void (^doDeviceRegistration)(void) = ^{ - // Asynchronous HTTP request - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/deviceRegistrations"]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [[self->_rest defaultEncoder] encodeLocalDevice:local error:nil]; - [request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"POST" + path:@"/push/deviceRegistrations" + baseUrl:self->_rest.baseUrl + params:nil + body:[self->_rest.defaultEncoder encodeLocalDevice:local error:nil] + headers:nil + error:&error]; + if (error) { + ARTLogError(self->_logger, @"%@: device registration failed (%@)", NSStringFromClass(self.class), error.localizedDescription); + [self sendEvent:[ARTPushActivationEventGettingDeviceRegistrationFailed newWithError:[ARTErrorInfo createFromNSError:error]]]; + return; + } ARTLogDebug(self->_logger, @"%@: device registration with request %@", NSStringFromClass(self.class), request); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:nil completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { @@ -252,14 +260,26 @@ - (void)deviceUpdateRegistration:(ARTErrorInfo *)error { return; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:local.id]]; - request.HTTPMethod = @"PATCH"; - request.HTTPBody = [[_rest defaultEncoder] encode:@{ + NSDictionary *bodyDict = @{ @"push": @{ @"recipient": local.push.recipient } - } error:nil]; - [request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + }; + + NSError *requestError = nil; + NSMutableURLRequest *request = [_rest buildRequest:@"PATCH" + path:[@"/push/deviceRegistrations" stringByAppendingPathComponent:local.id] + baseUrl:_rest.baseUrl + params:nil + body:bodyDict + headers:nil + error:&requestError]; + if (requestError) { + ARTLogError(self->_logger, @"%@: device update failed (%@)", NSStringFromClass(self.class), requestError.localizedDescription); + [self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:[ARTErrorInfo createFromNSError:requestError]]]; + return; + } + [request setDeviceAuthentication:local]; ARTLogDebug(_logger, @"%@: update device with request %@", NSStringFromClass(self.class), request); @@ -303,17 +323,26 @@ - (void)syncDevice { void (^doDeviceSync)(void) = ^{ // Asynchronous HTTP request - NSString *const path = [@"/push/deviceRegistrations" stringByAppendingPathComponent:local.id]; - NSMutableURLRequest *const request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:path]]; - request.HTTPMethod = @"PUT"; - request.HTTPBody = [[self->_rest defaultEncoder] encodeDeviceDetails:local error:nil]; - [request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + NSError *requestError = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"PUT" + path:[@"/push/deviceRegistrations" stringByAppendingPathComponent:local.id] + baseUrl:self->_rest.baseUrl + params:nil + body:[self->_rest.defaultEncoder encodeDeviceDetails:local error:nil] + headers:nil + error:&requestError]; + if (requestError) { + ARTLogError(self->_logger, @"%@: device sync failed (%@)", NSStringFromClass(self.class), requestError.localizedDescription); + [self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:[ARTErrorInfo createFromNSError:requestError]]]; + return; + } + [request setDeviceAuthentication:local]; ARTLogDebug(self->_logger, @"%@: sync device with request %@", NSStringFromClass(self.class), request); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:nil completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (error) { - ARTLogError(self->_logger, @"%@: device registration failed (%@)", NSStringFromClass(self.class), error.localizedDescription); + ARTLogError(self->_logger, @"%@: device sync failed (%@)", NSStringFromClass(self.class), error.localizedDescription); [self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:[ARTErrorInfo createFromNSError:error]]]; return; } @@ -361,8 +390,20 @@ - (void)deviceUnregistration:(ARTErrorInfo *)error { } // Asynchronous HTTP request - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:local.id]]; - request.HTTPMethod = @"DELETE"; + NSError *requestError = nil; + NSMutableURLRequest *request = [_rest buildRequest:@"DELETE" + path:[@"/push/deviceRegistrations" stringByAppendingPathComponent:local.id] + baseUrl:_rest.baseUrl + params:nil + body:nil + headers:nil + error:&requestError]; + if (requestError) { + ARTLogError(self->_logger, @"%@: device deregistration failed (%@)", NSStringFromClass(self.class), requestError.localizedDescription); + [self sendEvent:[ARTPushActivationEventDeregistrationFailed newWithError:[ARTErrorInfo createFromNSError:requestError]]]; + return; + } + [request setDeviceAuthentication:local]; ARTLogDebug(_logger, @"%@: device deregistration with request %@", NSStringFromClass(self.class), request); diff --git a/Source/ARTPushAdmin.m b/Source/ARTPushAdmin.m index 81b110e44..b50aa8707 100644 --- a/Source/ARTPushAdmin.m +++ b/Source/ARTPushAdmin.m @@ -75,13 +75,21 @@ - (void)publish:(ARTPushRecipient *)recipient data:(ARTJsonObject *)data wrapper return; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/publish"]]; - request.HTTPMethod = @"POST"; NSMutableDictionary *body = [NSMutableDictionary dictionary]; [body setObject:recipient forKey:@"recipient"]; [body addEntriesFromDictionary:data]; - request.HTTPBody = [[self->_rest defaultEncoder] encode:body error:nil]; - [request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"POST" + path:@"/push/publish" + baseUrl:self->_rest.baseUrl + params:nil + body:body + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; + } ARTLogDebug(self->_logger, @"push notification to a single device %@", request); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:wrapperSDKAgents completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { diff --git a/Source/ARTPushChannel.m b/Source/ARTPushChannel.m index 95bc551fe..8ff012da9 100644 --- a/Source/ARTPushChannel.m +++ b/Source/ARTPushChannel.m @@ -120,13 +120,24 @@ - (void)subscribeDeviceWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapp } NSString *deviceId = device.id; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [[self->_rest defaultEncoder] encode:@{ + NSDictionary *bodyDict = @{ @"deviceId": deviceId, @"channel": self->_channel.name, - } error:nil]; - [request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + }; + + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"POST" + path:@"/push/channelSubscriptions" + baseUrl:self->_rest.baseUrl + params:nil + body:bodyDict + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; + } + [request setDeviceAuthentication:deviceId localDevice:device]; ARTLogDebug(self->_logger, @"subscribe notifications for device %@ in channel %@", deviceId, self->_channel.name); @@ -155,13 +166,23 @@ - (void)subscribeClientWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapp return; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [[self->_rest defaultEncoder] encode:@{ + NSDictionary *bodyDict = @{ @"clientId": clientId, @"channel": self->_channel.name, - } error:nil]; - [request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + }; + + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"POST" + path:@"/push/channelSubscriptions" + baseUrl:self->_rest.baseUrl + params:nil + body:bodyDict + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; + } ARTLogDebug(self->_logger, @"subscribe notifications for clientId %@ in channel %@", clientId, self->_channel.name); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:wrapperSDKAgents completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { @@ -190,14 +211,24 @@ - (void)unsubscribeDeviceWithWrapperSDKAgents:(nullable NSStringDictionary *)wra } NSString *deviceId = device.id; - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; - components.queryItems = @[ - [NSURLQueryItem queryItemWithName:@"deviceId" value:deviceId], - [NSURLQueryItem queryItemWithName:@"channel" value:self->_channel.name], - ]; + NSMutableDictionary *params = @{ + @"deviceId": deviceId, + @"channel": self->_channel.name, + }.mutableCopy; + + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"DELETE" + path:@"/push/channelSubscriptions" + baseUrl:self->_rest.baseUrl + params:params + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; + } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"DELETE"; [request setDeviceAuthentication:deviceId localDevice:device]; ARTLogDebug(self->_logger, @"unsubscribe notifications for device %@ in channel %@", deviceId, self->_channel.name); @@ -226,14 +257,23 @@ - (void)unsubscribeClientWithWrapperSDKAgents:(nullable NSStringDictionary *)wra return; } - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; - components.queryItems = @[ - [NSURLQueryItem queryItemWithName:@"clientId" value:clientId], - [NSURLQueryItem queryItemWithName:@"channel" value:self->_channel.name], - ]; - - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"DELETE"; + NSMutableDictionary *params = @{ + @"clientId": clientId, + @"channel": self->_channel.name, + }.mutableCopy; + + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"DELETE" + path:@"/push/channelSubscriptions" + baseUrl:self->_rest.baseUrl + params:params + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; + } ARTLogDebug(self->_logger, @"unsubscribe notifications for clientId %@ in channel %@", clientId, self->_channel.name); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:wrapperSDKAgents completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { @@ -260,7 +300,7 @@ - (BOOL)listSubscriptions:(NSStringDictionary *)params __block BOOL ret; art_dispatch_sync(_queue, ^{ - NSMutableDictionary *mutableParams = params ? [NSMutableDictionary dictionaryWithDictionary:params] : [[NSMutableDictionary alloc] init]; + NSMutableDictionary *mutableParams = params.mutableCopy ?: [NSMutableDictionary dictionary]; if (!mutableParams[@"deviceId"] && !mutableParams[@"clientId"]) { if (errorPtr) { @@ -283,10 +323,21 @@ - (BOOL)listSubscriptions:(NSStringDictionary *)params mutableParams[@"concatFilters"] = @"true"; - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; - components.queryItems = [mutableParams art_asURLQueryItems]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"GET"; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:@"/push/channelSubscriptions" + baseUrl:nil + params:mutableParams + body:nil + headers:nil + error:&error]; + if (error) { + if (errorPtr) { + *errorPtr = error; + } + ret = NO; + return; + } ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { return [self->_rest.encoders[response.MIMEType] decodePushChannelSubscriptions:data error:error]; diff --git a/Source/ARTPushChannelSubscriptions.m b/Source/ARTPushChannelSubscriptions.m index 9dba08979..081b4898f 100644 --- a/Source/ARTPushChannelSubscriptions.m +++ b/Source/ARTPushChannelSubscriptions.m @@ -80,14 +80,26 @@ - (void)save:(ARTPushChannelSubscription *)channelSubscription wrapperSDKAgents: #endif art_dispatch_async(_queue, ^{ - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; + NSMutableDictionary *params = nil; if (self->_rest.options.pushFullWait) { - components.queryItems = @[[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + params = @{ + @"fullWait": @"true" + }.mutableCopy; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [[self->_rest defaultEncoder] encodePushChannelSubscription:channelSubscription error:nil]; - [request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + NSData *bodyData = [[self->_rest defaultEncoder] encodePushChannelSubscription:channelSubscription error:nil]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"POST" + path:@"/push/channelSubscriptions" + baseUrl:self->_rest.baseUrl + params:params + body:bodyData + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; + } + [request setDeviceAuthentication:channelSubscription.deviceId localDevice:local]; ARTLogDebug(self->_logger, @"save channel subscription with request %@", request); @@ -121,9 +133,18 @@ - (void)listChannelsWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapperS } art_dispatch_async(_queue, ^{ - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channels"] resolvingAgainstBaseURL:NO]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"GET"; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:@"/push/channels" + baseUrl:nil + params:nil + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, [ARTErrorInfo createFromNSError:error]); + return; + } ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { return [self->_rest.encoders[response.MIMEType] decode:data error:error]; @@ -143,11 +164,19 @@ - (void)list:(NSStringDictionary *)params wrapperSDKAgents:(nullable NSStringDic } art_dispatch_async(_queue, ^{ - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; - components.queryItems = [params art_asURLQueryItems]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"GET"; - + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:@"/push/channelSubscriptions" + baseUrl:nil + params:params + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, [ARTErrorInfo createFromNSError:error]); + return; + } + ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { return [self->_rest.encoders[response.MIMEType] decodePushChannelSubscriptions:data error:error]; }; @@ -197,13 +226,22 @@ - (void)removeWhere:(NSStringDictionary *)params wrapperSDKAgents:(nullable NSSt } - (void)_removeWhere:(NSStringDictionary *)params wrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents callback:(ARTCallback)callback { - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; - components.queryItems = [params art_asURLQueryItems]; + NSMutableDictionary *queryParams = [params mutableCopy]; if (_rest.options.pushFullWait) { - components.queryItems = [components.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + queryParams[@"fullWait"] = @"true"; + } + NSError *error = nil; + NSMutableURLRequest *request = [_rest buildRequest:@"DELETE" + path:@"/push/channelSubscriptions" + baseUrl:_rest.baseUrl + params:queryParams + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"DELETE"; #if TARGET_OS_IOS [request setDeviceAuthentication:[params objectForKey:@"deviceId"] localDevice:_rest.device_nosync]; #endif diff --git a/Source/ARTPushDeviceRegistrations.m b/Source/ARTPushDeviceRegistrations.m index 3454e5e30..22bb68b14 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -81,14 +81,26 @@ - (void)save:(ARTDeviceDetails *)deviceDetails wrapperSDKAgents:(nullable NSStri #endif art_dispatch_async(_queue, ^{ - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceDetails.id] resolvingAgainstBaseURL:NO]; + NSMutableDictionary *params = nil; if (self->_rest.options.pushFullWait) { - components.queryItems = @[[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + params = @{ + @"fullWait": @"true" + }.mutableCopy; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"PUT"; - request.HTTPBody = [[self->_rest defaultEncoder] encodeDeviceDetails:deviceDetails error:nil]; - [request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + NSData *bodyData = [[self->_rest defaultEncoder] encodeDeviceDetails:deviceDetails error:nil]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"PUT" + path:[@"/push/deviceRegistrations" stringByAppendingPathComponent:deviceDetails.id] + baseUrl:self->_rest.baseUrl + params:params + body:bodyData + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; + } + [request setDeviceAuthentication:deviceDetails.id localDevice:local logger:self->_logger]; ARTLogDebug(self->_logger, @"save device with request %@", request); @@ -135,8 +147,19 @@ - (void)get:(ARTDeviceId *)deviceId wrapperSDKAgents:(nullable NSStringDictionar #endif art_dispatch_async(_queue, ^{ - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceId]]; - request.HTTPMethod = @"GET"; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:[@"/push/deviceRegistrations" stringByAppendingPathComponent:deviceId] + baseUrl:self->_rest.baseUrl + params:nil + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, [ARTErrorInfo createFromNSError:error]); + return; + } + [request setDeviceAuthentication:deviceId localDevice:local logger:self->_logger]; ARTLogDebug(self->_logger, @"get device with request %@", request); @@ -181,10 +204,18 @@ - (void)list:(NSStringDictionary *)params wrapperSDKAgents:(nullable NSStringDic } art_dispatch_async(_queue, ^{ - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/deviceRegistrations"] resolvingAgainstBaseURL:NO]; - components.queryItems = [params art_asURLQueryItems]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"GET"; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:@"/push/deviceRegistrations" + baseUrl:self->_rest.baseUrl + params:params + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, [ARTErrorInfo createFromNSError:error]); + return; + } ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { return [self->_rest.encoders[response.MIMEType] decodeDevicesDetails:data error:error]; @@ -204,13 +235,24 @@ - (void)remove:(NSString *)deviceId wrapperSDKAgents:(nullable NSStringDictionar } art_dispatch_async(_queue, ^{ - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceId] resolvingAgainstBaseURL:NO]; - if (self->_rest.options.pushFullWait) { - components.queryItems = @[[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + NSMutableDictionary *params = nil; + if (self->_rest.options.pushFullWait) { + params = @{ + @"fullWait": @"true" + }.mutableCopy; + } + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"DELETE" + path:[@"/push/deviceRegistrations" stringByAppendingPathComponent:deviceId] + baseUrl:self->_rest.baseUrl + params:params + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"DELETE"; - [request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; ARTLogDebug(self->_logger, @"remove device with request %@", request); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:wrapperSDKAgents completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { @@ -248,13 +290,23 @@ - (void)removeWhere:(NSStringDictionary *)params wrapperSDKAgents:(nullable NSSt #endif art_dispatch_async(_queue, ^{ - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/deviceRegistrations"] resolvingAgainstBaseURL:NO]; - components.queryItems = [params art_asURLQueryItems]; + NSMutableDictionary *queryParams = [params mutableCopy]; if (self->_rest.options.pushFullWait) { - components.queryItems = [components.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + queryParams[@"fullWait"] = @"true"; + } + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"DELETE" + path:@"/push/deviceRegistrations" + baseUrl:self->_rest.baseUrl + params:queryParams + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"DELETE"; + [request setDeviceAuthentication:[params objectForKey:@"deviceId"] localDevice:local]; ARTLogDebug(self->_logger, @"remove devices with request %@", request); diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 6a829fdcc..e46713e1f 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -1207,8 +1207,17 @@ - (void)historyWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAge [self history:[[ARTRealtimeHistoryQuery alloc] init] wrapperSDKAgents:wrapperSDKAgents callback:callback error:nil]; } -- (BOOL)history:(ARTRealtimeHistoryQuery *)query wrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents callback:(ARTPaginatedMessagesCallback)callback error:(NSError **)errorPtr { - query.realtimeChannel = self; +- (BOOL)history:(ARTRealtimeHistoryQuery *)query +wrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents + callback:(ARTPaginatedMessagesCallback)callback + error:(NSError **)errorPtr { + if (query.untilAttach) { // RTL10b + if (self.state_nosync != ARTRealtimeChannelAttached) { + *errorPtr = [NSError errorWithDomain:ARTAblyErrorDomain code:ARTRealtimeHistoryErrorNotAttached userInfo:@{NSLocalizedDescriptionKey:@"ARTRealtimeHistoryQuery: untilAttach used in channel that isn't attached"}]; + return false; + } + query.realtimeChannelAttachSerial = self.attachSerial; + } return [_restChannel history:query wrapperSDKAgents:wrapperSDKAgents callback:callback error:errorPtr]; } diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index e6a20129e..2211952c2 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -279,7 +279,6 @@ - (void)historyWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAge } - (BOOL)history:(ARTRealtimeHistoryQuery *)query wrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents callback:(ARTPaginatedPresenceCallback)callback error:(NSError **)errorPtr { - query.realtimeChannel = _channel; return [_channel.restChannel.presence history:query wrapperSDKAgents:wrapperSDKAgents callback:callback error:errorPtr]; } diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 35d4862c4..f649432ad 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -495,7 +495,9 @@ - (NSString *)agentIdentifierWithWrapperSDKAgents:(nullable NSDictionary *)_timeWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents completion:(ARTDateTimeCallback)callback { - NSURL *requestUrl = [NSURL URLWithString:@"/time" relativeToURL:self.baseUrl]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl]; - request.HTTPMethod = @"GET"; - NSString *accept = [[_encoders.allValues valueForKeyPath:@"mimeType"] componentsJoinedByString:@","]; - [request setValue:accept forHTTPHeaderField:@"Accept"]; + NSError *error = nil; + NSMutableURLRequest *request = [self buildRequest:@"GET" + path:@"/time" + baseUrl:self.baseUrl + params:nil + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, error); + return nil; + } return [self executeAblyRequest:request withAuthOption:ARTAuthenticationOff wrapperSDKAgents:wrapperSDKAgents completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (error) { @@ -631,58 +640,45 @@ - (void)timeWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents }]; } -- (BOOL)request:(NSString *)method - path:(NSString *)path - params:(nullable NSStringDictionary *)params - body:(nullable id)body - headers:(nullable NSStringDictionary *)headers -wrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents - callback:(ARTHTTPPaginatedCallback)callback - error:(NSError **)errorPtr { - - if (callback) { - void (^userCallback)(ARTHTTPPaginatedResponse *, ARTErrorInfo *) = callback; - callback = ^(ARTHTTPPaginatedResponse *r, ARTErrorInfo *e) { - art_dispatch_async(self->_userQueue, ^{ - userCallback(r, e); - }); - }; - } - - if (![[method lowercaseString] isEqualToString:@"get"] && - ![[method lowercaseString] isEqualToString:@"post"] && - ![[method lowercaseString] isEqualToString:@"patch"] && - ![[method lowercaseString] isEqualToString:@"put"] && - ![[method lowercaseString] isEqualToString:@"delete"]) { +- (NSMutableURLRequest * _Nullable)buildRequest:(NSString *)method + path:(NSString *)path + baseUrl:(nullable NSURL *)baseUrl + params:(nullable NSStringDictionary *)params + body:(nullable id)body + headers:(nullable NSStringDictionary *)headers + error:(NSError *_Nullable *_Nullable)errorPtr { + if (![@[@"get", @"post", @"patch", @"put", @"delete"] containsObject:method.lowercaseString]) { if (errorPtr) { *errorPtr = [NSError errorWithDomain:ARTAblyErrorDomain code:ARTCustomRequestErrorInvalidMethod - userInfo:@{NSLocalizedDescriptionKey:@"Method isn't valid."}]; + userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"HTTP method '%@' isn't valid.", method]}]; } - return NO; + return nil; } - if (body && - ![body isKindOfClass:[NSDictionary class]] && - ![body isKindOfClass:[NSArray class]]) { + if (body && ![body isKindOfClass:NSData.class] && ![body isKindOfClass:NSDictionary.class] && ![body isKindOfClass:NSArray.class]) { if (errorPtr) { *errorPtr = [NSError errorWithDomain:ARTAblyErrorDomain code:ARTCustomRequestErrorInvalidBody - userInfo:@{NSLocalizedDescriptionKey:@"Body should be a Dictionary or an Array."}]; + userInfo:@{NSLocalizedDescriptionKey:@"Body should be a NSData, NSDictionary or an NSArray."}]; } - return NO; + return nil; } - if ([[path stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] isEqualToString:@""]) { + if ([[path stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet] isEqualToString:@""]) { if (errorPtr) { *errorPtr = [NSError errorWithDomain:ARTAblyErrorDomain code:ARTCustomRequestErrorInvalidPath userInfo:@{NSLocalizedDescriptionKey:@"Path cannot be empty."}]; } - return NO; + return nil; + } + + if (baseUrl == nil) { + baseUrl = self.baseUrl; } - NSURL *url = [NSURL URLWithString:path relativeToURL:self.baseUrl]; + NSURL *url = [NSURL URLWithString:path relativeToURL:baseUrl]; // Should not happen in iOS 17 and above. See explanation in the "Important" section here: // https://developer.apple.com/documentation/foundation/nsurl/1572047-urlwithstring if (!url) { @@ -691,7 +687,7 @@ - (BOOL)request:(NSString *)method code:ARTCustomRequestErrorInvalidPath userInfo:@{NSLocalizedDescriptionKey:@"Path isn't valid for an URL."}]; } - return NO; + return nil; } NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:YES]; @@ -705,25 +701,72 @@ - (BOOL)request:(NSString *)method components.queryItems = queryItems; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = method; + request.HTTPMethod = method.uppercaseString; [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { [request addValue:value forHTTPHeaderField:key]; }]; if (body != nil) { - NSError *encodeError = nil; - NSData *bodyData = [self.defaultEncoder encode:body error:&encodeError]; - + NSData *bodyData; + if ([body isKindOfClass:NSData.class]) { + bodyData = body; + } else { + NSError *encodeError = nil; + bodyData = [self.defaultEncoder encode:body error:&encodeError]; + if (encodeError) { + if (errorPtr) { + *errorPtr = encodeError; + } + return nil; + } + } request.HTTPBody = bodyData; - [request setValue:[self.defaultEncoder mimeType] forHTTPHeaderField:@"Content-Type"]; - if ([[method lowercaseString] isEqualToString:@"post"]) { + [request setValue:self.defaultEncoder.mimeType forHTTPHeaderField:@"Content-Type"]; + if ([@[@"post", @"patch", @"put"] containsObject:method.lowercaseString]) { [request setValue:[NSString stringWithFormat:@"%d", (unsigned int)bodyData.length] forHTTPHeaderField:@"Content-Length"]; } } + [request setTimeoutInterval:_options.httpRequestTimeout]; [request setAcceptHeader:self.defaultEncoder encoders:self.encoders]; + return request; +} + +- (BOOL)request:(NSString *)method + path:(NSString *)path + params:(nullable NSStringDictionary *)params + body:(nullable id)body + headers:(nullable NSStringDictionary *)headers +wrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents + callback:(ARTHTTPPaginatedCallback)callback + error:(NSError **)errorPtr { + + if (callback) { + void (^userCallback)(ARTHTTPPaginatedResponse *, ARTErrorInfo *) = callback; + callback = ^(ARTHTTPPaginatedResponse *r, ARTErrorInfo *e) { + art_dispatch_async(self->_userQueue, ^{ + userCallback(r, e); + }); + }; + } + + NSError *error = nil; + NSMutableURLRequest *request = [self buildRequest:method + path:path + baseUrl:self.baseUrl + params:params + body:body + headers:headers + error:&error]; + if (error) { + if (errorPtr) { + *errorPtr = error; + } + return NO; + } + ARTLogDebug(self.logger, @"request %@ %@", method, path); art_dispatch_async(_queue, ^{ [ARTHTTPPaginatedResponse executePaginated:self withRequest:request wrapperSDKAgents:wrapperSDKAgents logger:self.logger callback:callback]; @@ -750,7 +793,6 @@ - (BOOL)statsWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgent completion:(ARTPaginatedStatsCallback)callback { return [self stats:[[ARTStatsQuery alloc] init] wrapperSDKAgents:wrapperSDKAgents callback:callback error:nil]; } - - (BOOL)stats:(ARTStatsQuery *)query wrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents callback:(ARTPaginatedStatsCallback)callback error:(NSError **)errorPtr { if (callback) { ARTPaginatedStatsCallback userCallback = callback; @@ -778,16 +820,20 @@ - (BOOL)stats:(ARTStatsQuery *)query wrapperSDKAgents:(nullable NSStringDictiona return NO; } - NSURLComponents *requestUrl = [NSURLComponents componentsWithString:@"/stats"]; NSError *error = nil; - requestUrl.queryItems = [query asQueryItems:&error]; + NSMutableURLRequest *request = [self buildRequest:@"GET" + path:@"/stats" + baseUrl:self.baseUrl + params:query.asQueryParams + body:nil + headers:nil + error:&error]; if (error) { if (errorPtr) { *errorPtr = error; } return NO; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[requestUrl URLRelativeToURL:self.baseUrl]]; // Override X-Ably-Version header to use protocol version 2 for stats [request setValue:@"2" forHTTPHeaderField:@"X-Ably-Version"]; @@ -933,3 +979,4 @@ - (NSString *)deviceTokenString { } @end + diff --git a/Source/ARTRestAnnotations.m b/Source/ARTRestAnnotations.m index 346003cc6..102a3e0e7 100644 --- a/Source/ARTRestAnnotations.m +++ b/Source/ARTRestAnnotations.m @@ -5,6 +5,7 @@ #import "ARTDataQuery+Private.h" #import "ARTJsonEncoder.h" #import "ARTNSArray+ARTFunctional.h" +#import "ARTNSString+ARTUtil.h" #import "ARTChannel+Private.h" #import "ARTBaseMessage+Private.h" #import "ARTInternalLog.h" @@ -37,12 +38,10 @@ - (instancetype)initWithLimit:(NSUInteger)limit { return self; } -- (NSMutableArray *)asQueryItems { - NSMutableArray *items = [NSMutableArray array]; - - [items addObject:[NSURLQueryItem queryItemWithName:@"limit" value:[NSString stringWithFormat:@"%lu", (unsigned long)self.limit]]]; - - return items; +- (NSStringDictionary *)asQueryParams { + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + params[@"limit"] = [NSString stringWithFormat:@"%lu", (unsigned long)self.limit]; + return params; } @end @@ -187,15 +186,19 @@ - (void)publishAnnotation:(ARTOutboundAnnotation *)outboundAnnotation } // Construct URL for the annotation endpoint - NSString *path = [NSString stringWithFormat:@"%@/messages/%@/annotations", [self->_channel getBasePath], [messageSerial stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]]]; + NSString *path = [NSString stringWithFormat:@"%@/messages/%@/annotations", [self->_channel getBasePath], [messageSerial encodePathSegment]]; - NSURL *url = [NSURL URLWithString:path]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; - request.HTTPMethod = @"POST"; - request.HTTPBody = encodedAnnotation; - - if (self->_channel.rest.defaultEncoding) { - [request setValue:self->_channel.rest.defaultEncoding forHTTPHeaderField:@"Content-Type"]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_channel.rest buildRequest:@"POST" + path:path + baseUrl:nil + params:nil + body:encodedAnnotation + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; } ARTLogDebug(self->_logger, @"RS:%p CH:%p (%@) publish annotation %@", @@ -245,20 +248,21 @@ - (void)getForMessageSerial:(NSString *)messageSerial query:(ARTAnnotationsQuery } art_dispatch_async(_queue, ^{ - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString: [NSString stringWithFormat:@"%@/messages/%@/annotations", [self->_channel getBasePath], [messageSerial stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet]]] resolvingAgainstBaseURL:YES]; + NSString *path = [NSString stringWithFormat:@"%@/messages/%@/annotations", [self->_channel getBasePath], [messageSerial encodePathSegment]]; - // Add query parameters - NSMutableArray *queryItems = [NSMutableArray new]; - if (query) { - [queryItems addObjectsFromArray:[query asQueryItems]]; - } - if (queryItems.count > 0) { - components.queryItems = queryItems; + NSError *error = nil; + NSMutableURLRequest *request = [self->_channel.rest buildRequest:@"GET" + path:path + baseUrl:nil + params:[query asQueryParams] + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, [ARTErrorInfo createFromNSError:error]); + return; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:components.URL]; - request.HTTPMethod = @"GET"; - ARTLogDebug(self->_logger, @"RS:%p CH:%p (%@) get annotations request %@", self->_channel.rest, self->_channel, self->_channel.name, request); diff --git a/Source/ARTRestChannel.m b/Source/ARTRestChannel.m index b6ac49015..29ad93bb2 100644 --- a/Source/ARTRestChannel.m +++ b/Source/ARTRestChannel.m @@ -14,6 +14,8 @@ #import "ARTAuth+Private.h" #import "ARTTokenDetails.h" #import "ARTNSArray+ARTFunctional.h" +#import "ARTNSDictionary+ARTDictionaryUtil.h" +#import "ARTNSString+ARTUtil.h" #import "ARTPushChannel+Private.h" #import "ARTCrypto+Private.h" #import "ARTClientOptions.h" @@ -191,16 +193,13 @@ - (BOOL)history:(ARTDataQuery *)query wrapperSDKAgents:(nullable NSStringDiction }; } - __block BOOL ret; -art_dispatch_sync(_queue, ^{ if (query.limit > 1000) { if (errorPtr) { *errorPtr = [NSError errorWithDomain:ARTAblyErrorDomain code:ARTDataQueryErrorLimit userInfo:@{NSLocalizedDescriptionKey:@"Limit supports up to 1000 results only"}]; } - ret = NO; - return; + return NO; } if ([query.start compare:query.end] == NSOrderedDescending) { if (errorPtr) { @@ -208,23 +207,24 @@ - (BOOL)history:(ARTDataQuery *)query wrapperSDKAgents:(nullable NSStringDiction code:ARTDataQueryErrorTimestampRange userInfo:@{NSLocalizedDescriptionKey:@"Start must be equal to or less than end"}]; } - ret = NO; - return; + return NO; } - NSURLComponents *componentsUrl = [NSURLComponents componentsWithString:[self->_basePath stringByAppendingPathComponent:@"messages"]]; NSError *error = nil; - componentsUrl.queryItems = [query asQueryItems:&error]; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:[self->_basePath stringByAppendingPathComponent:@"messages"] + baseUrl:nil + params:query.asQueryParams + body:nil + headers:nil + error:&error]; if (error) { if (errorPtr) { *errorPtr = error; } - ret = NO; - return; + return NO; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:componentsUrl.URL]; - ARTPaginatedResultResponseProcessor responseProcessor = ^NSArray *(NSHTTPURLResponse *response, NSData *data, NSError **errorPtr) { id encoder = [self->_rest.encoders objectForKey:response.MIMEType]; return [[encoder decodeMessages:data error:errorPtr] artMap:^(ARTMessage *message) { @@ -237,12 +237,11 @@ - (BOOL)history:(ARTDataQuery *)query wrapperSDKAgents:(nullable NSStringDiction return message; }]; }; - - ARTLogDebug(self.logger, @"RS:%p C:%p (%@) stats request %@", self->_rest, self, self.name, request); + +art_dispatch_sync(_queue, ^{ [ARTPaginatedResult executePaginated:self->_rest withRequest:request andResponseProcessor:responseProcessor wrapperSDKAgents:wrapperSDKAgents logger:self.logger callback:callback]; - ret = YES; }); - return ret; + return YES; } - (void)status:(ARTChannelDetailsCallback)callback { @@ -255,8 +254,18 @@ - (void)status:(ARTChannelDetailsCallback)callback { }; } art_dispatch_async(_queue, ^{ - NSURL *url = [NSURL URLWithString:self->_basePath]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:self->_basePath + baseUrl:nil + params:nil + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, [ARTErrorInfo createFromNSError:error]); + return; + } ARTLogDebug(self.logger, @"RS:%p C:%p (%@) channel details request %@", self->_rest, self, self.name, request); @@ -317,78 +326,61 @@ - (void)internalPostMessages:(id)data callback:(ARTCallback)callback { } art_dispatch_async(_queue, ^{ - NSData *encodedMessage = nil; + NSArray *messages = nil; + BOOL dataIsArray = NO; - if ([data isKindOfClass:[ARTMessage class]]) { - ARTMessage *message = (ARTMessage *)data; - - NSString *baseId = nil; - if (self.rest.options.idempotentRestPublishing && message.isIdEmpty) { + if ([data isKindOfClass:ARTMessage.class]) { + messages = @[data]; + } + else if ([data isKindOfClass:[NSArray class]]) { + messages = data; + dataIsArray = YES; + } + else { + if (callback) callback([ARTErrorInfo createWithCode:ARTStateInvalidArgs message:@"Unknown data type for publishing messages. Expected ARTMessage or [ARTMessage]."]); + return; + } + + NSString *baseId = nil; + if (self.rest.options.idempotentRestPublishing) { + BOOL messagesHaveEmptyId = [messages artFilter:^BOOL(ARTMessage *m) { return !m.isIdEmpty; }].count <= 0; + if (messagesHaveEmptyId) { NSData *baseIdData = [ARTCrypto generateSecureRandomData:ARTIdempotentLibraryGeneratedIdLength]; baseId = [baseIdData base64EncodedStringWithOptions:0]; - message.id = [NSString stringWithFormat:@"%@:0", baseId]; } - + } + + NSInteger serial = 0; + for (ARTMessage *message in messages) { if (message.clientId && self.rest.auth.clientId_nosync && ![message.clientId isEqualToString:self.rest.auth.clientId_nosync]) { - callback([ARTErrorInfo createWithCode:ARTStateMismatchedClientId message:@"attempted to publish message with an invalid clientId"]); - return; - } - - NSError *encodeError = nil; - encodedMessage = [self.rest.defaultEncoder encodeMessage:message error:&encodeError]; - if (encodeError) { - callback([ARTErrorInfo createFromNSError:encodeError]); + if (callback) callback([ARTErrorInfo createWithCode:ARTStateMismatchedClientId message:@"attempted to publish message with an invalid clientId"]); return; } - } - else if ([data isKindOfClass:[NSArray class]]) { - NSArray *messages = (NSArray *)data; - - NSString *baseId = nil; - if (self.rest.options.idempotentRestPublishing) { - BOOL messagesHaveEmptyId = [messages artFilter:^BOOL(ARTMessage *m) { return !m.isIdEmpty; }].count <= 0; - if (messagesHaveEmptyId) { - NSData *baseIdData = [ARTCrypto generateSecureRandomData:ARTIdempotentLibraryGeneratedIdLength]; - baseId = [baseIdData base64EncodedStringWithOptions:0]; - } - } - - NSInteger serial = 0; - for (ARTMessage *message in messages) { - if (message.clientId && self.rest.auth.clientId_nosync && ![message.clientId isEqualToString:self.rest.auth.clientId_nosync]) { - callback([ARTErrorInfo createWithCode:ARTStateMismatchedClientId message:@"attempted to publish message with an invalid clientId"]); - return; - } - if (baseId) { - message.id = [NSString stringWithFormat:@"%@:%ld", baseId, (long)serial]; - } - serial += 1; - } - - NSError *encodeError = nil; - encodedMessage = [self.rest.defaultEncoder encodeMessages:data error:&encodeError]; - if (encodeError) { - callback([ARTErrorInfo createFromNSError:encodeError]); - return; + if (baseId) { + message.id = [NSString stringWithFormat:@"%@:%ld", baseId, (long)serial]; } + serial += 1; } - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:[self->_basePath stringByAppendingPathComponent:@"messages"]] resolvingAgainstBaseURL:YES]; - NSMutableArray *queryItems = [NSMutableArray new]; + NSArray *jsonMessages = [self.rest.defaultEncoder messagesToArray:messages]; - if (queryItems.count > 0) { - components.queryItems = queryItems; - } - - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = encodedMessage; + // unwrap single item array back to the object, because many tests rely on that + id bodyData = jsonMessages.count > 1 || dataIsArray ? jsonMessages : jsonMessages.firstObject; - if (self.rest.defaultEncoding) { - [request setValue:self.rest.defaultEncoding forHTTPHeaderField:@"Content-Type"]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"POST" + path:[self->_basePath stringByAppendingPathComponent:@"messages"] + baseUrl:nil + params:nil + body:bodyData + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; } - - ARTLogDebug(self.logger, @"RS:%p C:%p (%@) post message %@", self->_rest, self, self.name, [[NSString alloc] initWithData:encodedMessage ?: [NSData data] encoding:NSUTF8StringEncoding]); + + ARTLogDebug(self.logger, @"RS:%p C:%p (%@) post message(s):\n%@", self->_rest, self, self.name, jsonMessages); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:nil completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (callback) { @@ -469,14 +461,6 @@ - (void)_updateMessage:(ARTMessage *)message requestBody[@"extras"] = message.extras; } - // Encode the request body - NSError *encodeError = nil; - NSData *requestBodyData = [self.rest.defaultEncoder encode:requestBody error:&encodeError]; - if (encodeError) { - if (callback) callback([ARTErrorInfo createFromNSError:encodeError]); - return; - } - // RSL12b - PATCH to /channels/{channelName}/messages/{serial} // RSL13b - POST to /channels/{channelName}/messages/{serial}/delete NSString *messagePath; @@ -484,35 +468,34 @@ - (void)_updateMessage:(ARTMessage *)message if (isDelete) { // RSL13b - POST to /channels/{channelName}/messages/{serial}/delete - messagePath = [NSString stringWithFormat:@"messages/%@/delete", [message.serial stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]]; + messagePath = [NSString stringWithFormat:@"%@/messages/%@/delete", self->_basePath, [message.serial encodePathSegment]]; httpMethod = @"POST"; } else { // RSL12b - PATCH to /channels/{channelName}/messages/{serial} - messagePath = [NSString stringWithFormat:@"messages/%@", [message.serial stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]]; + messagePath = [NSString stringWithFormat:@"%@/messages/%@", self->_basePath, [message.serial encodePathSegment]]; httpMethod = @"PATCH"; } - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:[self->_basePath stringByAppendingPathComponent:messagePath]] resolvingAgainstBaseURL:YES]; - // RSL12a/RSL13a - params in querystring - if (params && params.count > 0) { - NSMutableArray *queryItems = [NSMutableArray array]; - for (NSString *key in params) { - [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:params[key].stringValue]]; - } - components.queryItems = queryItems; - } - - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = httpMethod; - request.HTTPBody = requestBodyData; + NSStringDictionary *queryParams = [params artMap:^id(NSString *key, ARTStringifiable *value) { + return value.stringValue; + }]; - if (self.rest.defaultEncoding) { - [request setValue:self.rest.defaultEncoding forHTTPHeaderField:@"Content-Type"]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:httpMethod + path:messagePath + baseUrl:nil + params:queryParams + body:requestBody + headers:nil + error:&error]; + if (error) { + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; } NSString *logOperation = isDelete ? @"delete" : @"update"; - ARTLogDebug(self.logger, @"RS:%p C:%p (%@) %@ message %@", self->_rest, self, self.name, logOperation, [[NSString alloc] initWithData:requestBodyData ?: [NSData data] encoding:NSUTF8StringEncoding]); + ARTLogDebug(self.logger, @"RS:%p C:%p (%@) %@ message:\n%@", self->_rest, self, self.name, logOperation, requestBody); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:wrapperSDKAgents completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (callback) { @@ -561,12 +544,21 @@ - (void)getMessageWithSerial:(NSString *)serial } art_dispatch_async(_queue, ^{ - NSString *messagePath = [NSString stringWithFormat:@"messages/%@", [serial stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]]; - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:[self->_basePath stringByAppendingPathComponent:messagePath]] resolvingAgainstBaseURL:YES]; - - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"GET"; + NSString *messagePath = [NSString stringWithFormat:@"%@/messages/%@", self->_basePath, [serial encodePathSegment]]; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:messagePath + baseUrl:nil + params:nil + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, [ARTErrorInfo createFromNSError:error]); + return; + } + ARTLogDebug(self.logger, @"RS:%p C:%p (%@) get message with serial %@", self->_rest, self, self.name, serial); [self->_rest executeAblyRequest:request withAuthOption:ARTAuthenticationOn wrapperSDKAgents:wrapperSDKAgents completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { @@ -636,11 +628,20 @@ - (void)getMessageVersionsWithSerial:(NSString *)serial } art_dispatch_async(_queue, ^{ - NSString *messagePath = [NSString stringWithFormat:@"messages/%@/versions", [serial stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]]; - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:[self->_basePath stringByAppendingPathComponent:messagePath]] resolvingAgainstBaseURL:YES]; + NSString *path = [NSString stringWithFormat:@"%@/messages/%@/versions", self->_basePath, [serial encodePathSegment]]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; - request.HTTPMethod = @"GET"; + NSError *error = nil; + NSMutableURLRequest *request = [self->_rest buildRequest:@"GET" + path:path + baseUrl:nil + params:nil + body:nil + headers:nil + error:&error]; + if (error) { + if (callback) callback(nil, [ARTErrorInfo createFromNSError:error]); + return; + } ARTLogDebug(self.logger, @"RS:%p C:%p (%@) get message versions for serial %@", self->_rest, self, self.name, serial); diff --git a/Source/ARTRestPresence.m b/Source/ARTRestPresence.m index 06bd53764..71183603d 100644 --- a/Source/ARTRestPresence.m +++ b/Source/ARTRestPresence.m @@ -35,17 +35,17 @@ - (instancetype)initWithLimit:(NSUInteger)limit clientId:(NSString *)clientId co return self; } -- (NSMutableArray *)asQueryItems { - NSMutableArray *items = [NSMutableArray array]; +- (NSStringDictionary *)asQueryParams { + NSMutableDictionary *items = [NSMutableDictionary dictionary]; if (self.clientId) { - [items addObject:[NSURLQueryItem queryItemWithName:@"clientId" value:self.clientId]]; + items[@"clientId"] = self.clientId; } if (self.connectionId) { - [items addObject:[NSURLQueryItem queryItemWithName:@"connectionId" value:self.connectionId]]; + items[@"connectionId"] = self.connectionId; } - - [items addObject:[NSURLQueryItem queryItemWithName:@"limit" value:[NSString stringWithFormat:@"%lu", (unsigned long)self.limit]]]; + + items[@"limit"] = [NSString stringWithFormat:@"%lu", (unsigned long)self.limit]; return items; } @@ -139,10 +139,21 @@ - (BOOL)get:(ARTPresenceQuery *)query callback:(ARTPaginatedPresenceCallback)cal } return NO; } - - NSURLComponents *requestUrl = [NSURLComponents componentsWithString:[_channel.basePath stringByAppendingPathComponent:@"presence"]]; - requestUrl.queryItems = [query asQueryItems]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl.URL]; + + NSError *error = nil; + NSMutableURLRequest *request = [self->_channel.rest buildRequest:@"GET" + path:[_channel.basePath stringByAppendingPathComponent:@"presence"] + baseUrl:nil + params:query.asQueryParams + body:nil + headers:nil + error:&error]; + if (error) { + if (errorPtr) { + *errorPtr = error; + } + return NO; + } ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **errorPtr) { id encoder = [self->_channel.rest.encoders objectForKey:response.MIMEType]; @@ -194,16 +205,20 @@ - (BOOL)history:(ARTDataQuery *)query wrapperSDKAgents:(nullable NSStringDiction return NO; } - NSURLComponents *requestUrl = [NSURLComponents componentsWithString:[_channel.basePath stringByAppendingPathComponent:@"presence/history"]]; NSError *error = nil; - requestUrl.queryItems = [query asQueryItems:&error]; + NSMutableURLRequest *request = [self->_channel.rest buildRequest:@"GET" + path:[_channel.basePath stringByAppendingPathComponent:@"presence/history"] + baseUrl:nil + params:query.asQueryParams + body:nil + headers:nil + error:&error]; if (error) { if (errorPtr) { *errorPtr = error; } return NO; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl.URL]; ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **errorPtr) { id encoder = [self->_channel.rest.encoders objectForKey:response.MIMEType]; @@ -225,3 +240,4 @@ - (BOOL)history:(ARTDataQuery *)query wrapperSDKAgents:(nullable NSStringDiction } @end + diff --git a/Source/ARTStats.m b/Source/ARTStats.m index 0383ca163..6d587fa33 100644 --- a/Source/ARTStats.m +++ b/Source/ARTStats.m @@ -25,12 +25,9 @@ - (instancetype)init { } } -- (NSMutableArray *)asQueryItems:(NSError **)error { - NSMutableArray *items = [super asQueryItems:error]; - if (*error) { - return nil; - } - [items addObject:[NSURLQueryItem queryItemWithName:@"unit" value:statsUnitToString(self.unit)]]; +- (NSStringDictionary *)asQueryParams { + NSMutableDictionary *items = (NSMutableDictionary *)[super asQueryParams]; + items[@"unit"] = statsUnitToString(self.unit); return items; } diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index ec895744c..e68bbbba0 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -323,21 +323,6 @@ - (NSString *)description { } } -@implementation NSDictionary (ARTURLQueryItemAdditions) - -- (NSArray *)art_asURLQueryItems { - NSMutableArray *items = [NSMutableArray new]; - for (id key in [self allKeys]) { - id value = [self valueForKey:key]; - if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) { - [items addObject:[NSURLQueryItem queryItemWithName:key value:value]]; - } - } - return items; -} - -@end - @implementation NSMutableArray (ARTQueueAdditions) - (void)art_enqueue:(id)object { diff --git a/Source/PrivateHeaders/Ably/ARTDataQuery+Private.h b/Source/PrivateHeaders/Ably/ARTDataQuery+Private.h index 1c2c55fe6..4af44141f 100755 --- a/Source/PrivateHeaders/Ably/ARTDataQuery+Private.h +++ b/Source/PrivateHeaders/Ably/ARTDataQuery+Private.h @@ -5,13 +5,13 @@ NS_ASSUME_NONNULL_BEGIN @interface ARTDataQuery (Private) -- (nullable NSMutableArray /* */ *)asQueryItems:(NSError *_Nullable *)error; +- (NSStringDictionary *)asQueryParams; @end @interface ARTRealtimeHistoryQuery () -@property (readwrite) ARTRealtimeChannelInternal *realtimeChannel; +@property (readwrite, copy) NSString *realtimeChannelAttachSerial; @end diff --git a/Source/PrivateHeaders/Ably/ARTEncoder.h b/Source/PrivateHeaders/Ably/ARTEncoder.h index 8295c851b..0b6108426 100644 --- a/Source/PrivateHeaders/Ably/ARTEncoder.h +++ b/Source/PrivateHeaders/Ably/ARTEncoder.h @@ -48,6 +48,7 @@ NS_ASSUME_NONNULL_BEGIN // Message - (nullable NSData *)encodeMessage:(ARTMessage *)message error:(NSError *_Nullable *_Nullable)error; - (nullable ARTMessage *)decodeMessage:(NSData *)data error:(NSError *_Nullable *_Nullable)error; +- (NSDictionary *)messageToDictionary:(ARTMessage *)message; // Annotation - (nullable NSData *)encodeAnnotation:(ARTAnnotation *)annotation error:(NSError *_Nullable *_Nullable)error; @@ -56,6 +57,7 @@ NS_ASSUME_NONNULL_BEGIN // Message list - (nullable NSData *)encodeMessages:(NSArray *)messages error:(NSError *_Nullable *_Nullable)error; - (nullable NSArray *)decodeMessages:(NSData *)data error:(NSError *_Nullable *_Nullable)error; +- (NSArray *)messagesToArray:(NSArray *)messages; // Annotation list - (nullable NSData *)encodeAnnotations:(NSArray *)annotations error:(NSError *_Nullable *_Nullable)error; diff --git a/Source/PrivateHeaders/Ably/ARTNSString+ARTUtil.h b/Source/PrivateHeaders/Ably/ARTNSString+ARTUtil.h index 515e5797a..049e18ac7 100644 --- a/Source/PrivateHeaders/Ably/ARTNSString+ARTUtil.h +++ b/Source/PrivateHeaders/Ably/ARTNSString+ARTUtil.h @@ -7,5 +7,6 @@ + (NSString *)nilToEmpty:(NSString*)aString; - (BOOL)isEmptyString; - (BOOL)isNotEmptyString; +- (NSString *)encodePathSegment; @end diff --git a/Source/PrivateHeaders/Ably/ARTPresence+Private.h b/Source/PrivateHeaders/Ably/ARTPresence+Private.h index 2ed287257..ed2e765a1 100644 --- a/Source/PrivateHeaders/Ably/ARTPresence+Private.h +++ b/Source/PrivateHeaders/Ably/ARTPresence+Private.h @@ -1,12 +1,6 @@ #import #import "ARTChannel.h" -@interface ARTPresenceQuery () - -- (NSMutableArray *)asQueryItems; - -@end - @interface ARTPresence () @property (readonly, getter=getChannel) ARTChannel *channel; diff --git a/Source/PrivateHeaders/Ably/ARTRest+Private.h b/Source/PrivateHeaders/Ably/ARTRest+Private.h index 2cbb66500..b45c840b7 100644 --- a/Source/PrivateHeaders/Ably/ARTRest+Private.h +++ b/Source/PrivateHeaders/Ably/ARTRest+Private.h @@ -100,6 +100,14 @@ NS_ASSUME_NONNULL_BEGIN - (void)timeWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents completion:(ARTDateTimeCallback)callback; +- (NSMutableURLRequest * _Nullable)buildRequest:(NSString *)method + path:(NSString *)path + baseUrl:(nullable NSURL *)baseUrl + params:(nullable NSStringDictionary *)params + body:(nullable id)body + headers:(nullable NSStringDictionary *)headers + error:(NSError *_Nullable *_Nullable)errorPtr; + - (BOOL)request:(NSString *)method path:(NSString *)path params:(nullable NSStringDictionary *)params diff --git a/Source/include/Ably/ARTTypes.h b/Source/include/Ably/ARTTypes.h index 756decff3..1538d4cdf 100644 --- a/Source/include/Ably/ARTTypes.h +++ b/Source/include/Ably/ARTTypes.h @@ -188,7 +188,7 @@ typedef NS_ENUM(NSInteger, ARTDataQueryError) { /// :nodoc: NS_SWIFT_SENDABLE typedef NS_ENUM(NSInteger, ARTRealtimeHistoryError) { - ARTRealtimeHistoryErrorNotAttached = ARTDataQueryErrorTimestampRange + 1 + ARTRealtimeHistoryErrorNotAttached = ARTDataQueryErrorTimestampRange + 1 // TODO: overlaps with ARTDataQueryErrorMissingRequiredFields }; /// :nodoc: @@ -467,11 +467,6 @@ NS_SWIFT_SENDABLE @interface NSURL (ARTLog) @end -/// :nodoc: -@interface NSDictionary (ARTURLQueryItemAdditions) -@property (nonatomic, readonly) NSArray *art_asURLQueryItems; -@end - /// :nodoc: @interface NSMutableArray (ARTQueueAdditions) - (void)art_enqueue:(id)object; @@ -485,6 +480,7 @@ NS_SWIFT_SENDABLE /// :nodoc: typedef NSDictionary NSStringDictionary; +typedef NSMutableDictionary NSMutableStringDictionary; // Below are the typedefs of completion handlers to improve readability and maintainability in properties and method parameters. // Either result/response or error can be nil but not both. diff --git a/Test/AblyTests/Tests/RestClientTests.swift b/Test/AblyTests/Tests/RestClientTests.swift index 8d1002aba..bc3686d01 100644 --- a/Test/AblyTests/Tests/RestClientTests.swift +++ b/Test/AblyTests/Tests/RestClientTests.swift @@ -1907,7 +1907,7 @@ class RestClientTests: XCTestCase { } } catch let error as NSError { XCTAssertEqual(error.code, ARTCustomRequestError.invalidMethod.rawValue) - expect(error.localizedDescription).to(contain("Method isn't valid")) + expect(error.localizedDescription).to(contain("HTTP method 'A' isn't valid")) } do { @@ -1916,7 +1916,7 @@ class RestClientTests: XCTestCase { } } catch let error as NSError { XCTAssertEqual(error.code, ARTCustomRequestError.invalidMethod.rawValue) - expect(error.localizedDescription).to(contain("Method isn't valid")) + expect(error.localizedDescription).to(contain("HTTP method '' isn't valid")) } } @@ -1946,7 +1946,7 @@ class RestClientTests: XCTestCase { } } catch let error as NSError { XCTAssertEqual(error.code, ARTCustomRequestError.invalidBody.rawValue) - expect(error.localizedDescription).to(contain("should be a Dictionary or an Array")) + expect(error.localizedDescription).to(contain("should be a NSData, NSDictionary or an NSArray")) } } From 12edeed05c2e1426008c5fe232d9b596ed33dcaa Mon Sep 17 00:00:00 2001 From: Marat Alekperov Date: Sat, 3 Jan 2026 23:51:47 +0000 Subject: [PATCH 2/2] Fix error handling in ARTRealtimeChannel's history method to ensure errorPtr is checked before assignment. Update asQueryParams to use mutableCopy to ensure the code won't break if the parent implementation changes to return an immutable copy. --- Source/ARTRealtimeChannel.m | 4 +++- Source/ARTStats.m | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index e46713e1f..26e1d4318 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -1213,7 +1213,9 @@ - (BOOL)history:(ARTRealtimeHistoryQuery *)query error:(NSError **)errorPtr { if (query.untilAttach) { // RTL10b if (self.state_nosync != ARTRealtimeChannelAttached) { - *errorPtr = [NSError errorWithDomain:ARTAblyErrorDomain code:ARTRealtimeHistoryErrorNotAttached userInfo:@{NSLocalizedDescriptionKey:@"ARTRealtimeHistoryQuery: untilAttach used in channel that isn't attached"}]; + if (errorPtr) { + *errorPtr = [NSError errorWithDomain:ARTAblyErrorDomain code:ARTRealtimeHistoryErrorNotAttached userInfo:@{NSLocalizedDescriptionKey:@"ARTRealtimeHistoryQuery: untilAttach used in channel that isn't attached"}]; + } return false; } query.realtimeChannelAttachSerial = self.attachSerial; diff --git a/Source/ARTStats.m b/Source/ARTStats.m index 6d587fa33..c5f0cb346 100644 --- a/Source/ARTStats.m +++ b/Source/ARTStats.m @@ -26,7 +26,7 @@ - (instancetype)init { } - (NSStringDictionary *)asQueryParams { - NSMutableDictionary *items = (NSMutableDictionary *)[super asQueryParams]; + NSMutableDictionary *items = (NSMutableDictionary *)[super asQueryParams].mutableCopy; items[@"unit"] = statsUnitToString(self.unit); return items; }