Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Source/HTTP/Apple/http_apple.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@

NAMESPACE_XBOX_HTTP_CLIENT_BEGIN

class AppleHttpSessionManager;

class AppleHttpProvider : public IHttpProvider
{
public:
AppleHttpProvider();
~AppleHttpProvider();

HRESULT PerformAsync(
HCCallHandle callHandle,
XAsyncBlock *async
) noexcept override;

private:
std::shared_ptr<AppleHttpSessionManager> m_httpSessionManager;
};

NAMESPACE_XBOX_HTTP_CLIENT_END
204 changes: 147 additions & 57 deletions Source/HTTP/Apple/http_apple.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,148 @@

NAMESPACE_XBOX_HTTP_CLIENT_BEGIN

class http_task_apple
struct AppleHttpTaskContext
{
public:
http_task_apple(_Inout_ XAsyncBlock* asyncBlock, _In_ HCCallHandle call);
bool initiate_request();

private:
void completion_handler(NSURLResponse* response, NSError* error);

HCCallHandle m_call; // non owning
XAsyncBlock* m_asyncBlock; // non owning

};

struct AppleHttpSession
{
NSURLSession* m_session;
NSURLSessionTask* m_sessionTask;
SessionDelegate* m_delegate;
std::unordered_map<NSUInteger, AppleHttpTaskContext> m_httpTaskContexts;
};

http_task_apple::http_task_apple(_Inout_ XAsyncBlock* asyncBlock, _In_ HCCallHandle call) :
m_call(call),
m_asyncBlock(asyncBlock),
m_sessionTask(nullptr)
class AppleHttpSessionManager : public std::enable_shared_from_this<AppleHttpSessionManager>
{
NSURLSessionConfiguration* configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration;
public:
AppleHttpSessionManager() = default;
~AppleHttpSessionManager();

HRESULT InitiateRequest(
HCCallHandle call,
XAsyncBlock *async
) noexcept;

private:
std::mutex m_httpSessionsMutex;
std::unordered_map<uint32_t, AppleHttpSession> m_httpSessions;

void StartTaskOnSession(HCCallHandle call, XAsyncBlock* asyncBlock, NSURLRequest* request);
void CompletionHandler(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error);
};

AppleHttpSessionManager::~AppleHttpSessionManager()
{
std::unique_lock<std::mutex> uniqueLock(m_httpSessionsMutex);

for (auto it = m_httpSessions.begin(); it != m_httpSessions.end(); ++it)
{
[it->second.m_session finishTasksAndInvalidate];
}
}

void AppleHttpSessionManager::StartTaskOnSession(HCCallHandle call, XAsyncBlock* asyncBlock, NSURLRequest* request)
{
uint32_t timeoutInSeconds = 0;
if (FAILED(HCHttpCallRequestGetTimeout(m_call, &timeoutInSeconds)))
if (FAILED(HCHttpCallRequestGetTimeout(call, &timeoutInSeconds)))
{
// default to 60 to match other default ios behaviour
timeoutInSeconds = 60;
}

[configuration setTimeoutIntervalForRequest:(NSTimeInterval)timeoutInSeconds];
[configuration setTimeoutIntervalForResource:(NSTimeInterval)timeoutInSeconds];

SessionDelegate* delegate = [SessionDelegate sessionDelegateWithHCCallHandle:m_call andCompletionHandler:^(NSURLResponse *response, NSError *error) {
std::unique_ptr<http_task_apple> me{this};
me->completion_handler(response, error);
}];
m_session = [NSURLSession sessionWithConfiguration:configuration delegate:delegate delegateQueue:nil];

{
std::unique_lock<std::mutex> uniqueLock(m_httpSessionsMutex);

auto httpSessionIter = m_httpSessions.find(timeoutInSeconds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why the key for this map is timeoutInSeconds, couldn't this collide, like, a lot?

@lawrencewuskyboxlabs lawrencewuskyboxlabs Mar 19, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because with the original implementation of each call causing a session to be made, the only differences in those sessions were:

  • how the delegate handled events (writing to a particular call's response body, triggering some completion handler, etc.)
  • the timeout the session would allow for a task running on it

The delegate's handling is pretty self-contained and the session itself doesn't really interact with its details. However the timeout is something that is defined in the session's config and can't be changed after the session is created. So I figured this would be the best way to identify sessions and choose which ones can be reused among calls; if a session already exists with the same timeout, then the call's corresponding task can run on it and let the delegate handle the per-task specifics.

if (httpSessionIter == m_httpSessions.end())
{
NSURLSessionConfiguration* configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration;
[configuration setTimeoutIntervalForRequest:(NSTimeInterval)timeoutInSeconds];
[configuration setTimeoutIntervalForResource:(NSTimeInterval)timeoutInSeconds];

std::weak_ptr<AppleHttpSessionManager> weak_this = shared_from_this();

SessionDelegate* delegate = [SessionDelegate sessionDelegateWithCompletionHandler:^(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse *response, NSError *error) {
if (auto me = weak_this.lock())
{
me->CompletionHandler(sessionTimeout, taskIdentifier, response, error);
}
}];
NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration delegate:delegate delegateQueue:nil];

httpSessionIter = m_httpSessions.emplace(timeoutInSeconds, AppleHttpSession{ .m_session = session, .m_delegate = delegate }).first;
}

NSURLSessionTask* sessionTask = [httpSessionIter->second.m_session dataTaskWithRequest:request];
NSUInteger taskIdentifier = [sessionTask taskIdentifier];

if (httpSessionIter->second.m_httpTaskContexts.count(taskIdentifier) > 0)
{
HC_TRACE_ERROR(HTTPCLIENT, "Shared session with timeout %u already has task with identifier %lu", timeoutInSeconds, taskIdentifier);
[sessionTask cancel];
return;
}

bool delegateRegistered = [httpSessionIter->second.m_delegate registerContextForTask:taskIdentifier withCall:call];
if (!delegateRegistered)
{
HC_TRACE_ERROR(HTTPCLIENT, "Shared session with timeout %u failed to register task with identifier %lu", timeoutInSeconds, taskIdentifier);
[sessionTask cancel];
return;
}

httpSessionIter->second.m_httpTaskContexts.emplace(taskIdentifier, AppleHttpTaskContext{ .m_call = call, .m_asyncBlock = asyncBlock });
[sessionTask resume];
}
}

void http_task_apple::completion_handler(NSURLResponse* response, NSError* error)
void AppleHttpSessionManager::CompletionHandler(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)
{
AppleHttpTaskContext taskContext;
{
std::unique_lock<std::mutex> uniqueLock(m_httpSessionsMutex);

auto httpSessionIter = m_httpSessions.find(sessionTimeout);
if (httpSessionIter == m_httpSessions.end())
{
HC_TRACE_ERROR(HTTPCLIENT, "No existing session with timeout %u", sessionTimeout);
return;
}

auto taskContextIter = httpSessionIter->second.m_httpTaskContexts.find(taskIdentifier);
if (taskContextIter == httpSessionIter->second.m_httpTaskContexts.end())
{
HC_TRACE_ERROR(HTTPCLIENT, "No existing task context with identifier %lu", taskIdentifier);
return;
}

taskContext = taskContextIter->second;

if (httpSessionIter->second.m_httpTaskContexts.size() > 1)
{
httpSessionIter->second.m_httpTaskContexts.erase(taskContextIter);
}
else
{
[httpSessionIter->second.m_session finishTasksAndInvalidate];
m_httpSessions.erase(httpSessionIter);
}
}

if (error)
{
uint32_t errorCode = static_cast<uint32_t>([error code]);
HC_TRACE_ERROR(HTTPCLIENT, "HCHttpCallPerform [ID %u] error from NSURLRequest code: %u", HCHttpCallGetId(m_call), errorCode);
HC_TRACE_ERROR(HTTPCLIENT, "HCHttpCallPerform [ID %u] error from NSURLRequest code: %u", HCHttpCallGetId(taskContext.m_call), errorCode);
HRESULT errorResult = E_FAIL;
if ([[error domain] isEqualToString:NSURLErrorDomain] && [error code] == NSURLErrorNotConnectedToInternet)
{
errorResult = E_HC_NO_NETWORK;
}

HCHttpCallResponseSetNetworkErrorCode(m_call, errorResult, errorCode);
XAsyncComplete(m_asyncBlock, errorResult, 0);
HCHttpCallResponseSetNetworkErrorCode(taskContext.m_call, errorResult, errorCode);
XAsyncComplete(taskContext.m_asyncBlock, errorResult, 0);
return;
}

Expand All @@ -72,7 +160,7 @@

uint32_t statusCode = static_cast<uint32_t>([httpResponse statusCode]);

HCHttpCallResponseSetStatusCode(m_call, statusCode);
HCHttpCallResponseSetStatusCode(taskContext.m_call, statusCode);

NSDictionary* headers = [httpResponse allHeaderFields];
for (NSString* key in headers)
Expand All @@ -81,21 +169,24 @@

char const* keyCString = [key cStringUsingEncoding:NSUTF8StringEncoding];
char const* valueCString = [value cStringUsingEncoding:NSUTF8StringEncoding];
HCHttpCallResponseSetHeader(m_call, keyCString, valueCString);
HCHttpCallResponseSetHeader(taskContext.m_call, keyCString, valueCString);
}

XAsyncComplete(m_asyncBlock, S_OK, 0);
XAsyncComplete(taskContext.m_asyncBlock, S_OK, 0);
}

bool http_task_apple::initiate_request()
HRESULT AppleHttpSessionManager::InitiateRequest(
_In_ HCCallHandle call,
_Inout_ XAsyncBlock* asyncBlock
) noexcept
{
char const* urlCString = nullptr;
char const* methodCString = nullptr;
if (FAILED(HCHttpCallRequestGetUrl(m_call, &methodCString, &urlCString)))
if (FAILED(HCHttpCallRequestGetUrl(call, &methodCString, &urlCString)))
{
HCHttpCallResponseSetNetworkErrorCode(m_call, E_FAIL, 0);
XAsyncComplete(m_asyncBlock, E_FAIL, 0);
return false;
HCHttpCallResponseSetNetworkErrorCode(call, E_FAIL, 0);
XAsyncComplete(asyncBlock, E_FAIL, 0);
return S_OK;
}

NSString* urlString = [[NSString alloc] initWithUTF8String:urlCString];
Expand All @@ -107,18 +198,18 @@
[request setHTTPMethod:methodString];

uint32_t numHeaders = 0;
if (FAILED(HCHttpCallRequestGetNumHeaders(m_call, &numHeaders)))
if (FAILED(HCHttpCallRequestGetNumHeaders(call, &numHeaders)))
{
HCHttpCallResponseSetNetworkErrorCode(m_call, E_FAIL, 0);
XAsyncComplete(m_asyncBlock, E_FAIL, 0);
return false;
HCHttpCallResponseSetNetworkErrorCode(call, E_FAIL, 0);
XAsyncComplete(asyncBlock, E_FAIL, 0);
return S_OK;
}

for (uint32_t i = 0; i<numHeaders; ++i)
{
char const* headerName;
char const* headerValue;
if (SUCCEEDED(HCHttpCallRequestGetHeaderAtIndex(m_call, i, &headerName, &headerValue)))
if (SUCCEEDED(HCHttpCallRequestGetHeaderAtIndex(call, i, &headerName, &headerValue)))
{
NSString* headerNameString = [[NSString alloc] initWithUTF8String:headerName];
NSString* headerValueString = [[NSString alloc] initWithUTF8String:headerValue];
Expand All @@ -130,37 +221,36 @@
HCHttpCallRequestBodyReadFunction readFunction = nullptr;
size_t requestBodySize = 0;
void* context = nullptr;
if (FAILED(HCHttpCallRequestGetRequestBodyReadFunction(m_call, &readFunction, &requestBodySize, &context))
if (FAILED(HCHttpCallRequestGetRequestBodyReadFunction(call, &readFunction, &requestBodySize, &context))
|| readFunction == nullptr)
{
HCHttpCallResponseSetNetworkErrorCode(m_call, E_FAIL, 0);
XAsyncComplete(m_asyncBlock, E_FAIL, 0);
return false;
HCHttpCallResponseSetNetworkErrorCode(call, E_FAIL, 0);
XAsyncComplete(asyncBlock, E_FAIL, 0);
return S_OK;
}

if (requestBodySize > 0)
{
[request setHTTPBodyStream:[RequestBodyStream requestBodyStreamWithHCCallHandle:m_call]];
[request setHTTPBodyStream:[RequestBodyStream requestBodyStreamWithHCCallHandle:call]];
[request addValue:[NSString stringWithFormat:@"%zu", requestBodySize] forHTTPHeaderField:@"Content-Length"];
}

StartTaskOnSession(call, asyncBlock, request);
return S_OK;
}

m_sessionTask = [m_session dataTaskWithRequest:request];
[m_sessionTask resume];
return true;
AppleHttpProvider::AppleHttpProvider() : m_httpSessionManager(std::make_shared<AppleHttpSessionManager>())
{
}

AppleHttpProvider::~AppleHttpProvider() = default;

HRESULT AppleHttpProvider::PerformAsync(
_In_ HCCallHandle call,
_Inout_ XAsyncBlock* asyncBlock
) noexcept
{
std::unique_ptr<xbox::httpclient::http_task_apple> httpTask(new xbox::httpclient::http_task_apple(asyncBlock, call));
HCHttpCallSetContext(call, &httpTask);
if (httpTask->initiate_request())
{
httpTask.release();
}
return S_OK;
return m_httpSessionManager->InitiateRequest(call, asyncBlock);
}

NAMESPACE_XBOX_HTTP_CLIENT_END
6 changes: 2 additions & 4 deletions Source/HTTP/Apple/session_delegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

@interface SessionDelegate : NSObject<NSURLSessionTaskDelegate, NSURLSessionDelegate, NSURLSessionDataDelegate>

@property (nonatomic, retain) NSMutableData *dataToDownload;
@property (nonatomic) float downloadSize;

+ (SessionDelegate*) sessionDelegateWithHCCallHandle:(HCCallHandle) call andCompletionHandler:(void(^)(NSURLResponse* response, NSError* error)) completion;
+ (SessionDelegate*) sessionDelegateWithCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completion;
+ (void) reportProgress:(HCCallHandle)call progressReportFunction:(HCHttpCallProgressReportFunction)progressReportFunction minimumInterval:(size_t)minimumInterval current:(size_t)current total:(size_t)total progressReportCallbackContext:(void*)progressReportCallbackContext lastProgressReport:(std::chrono::steady_clock::time_point*)lastProgressReport;
- (bool) registerContextForTask:(NSUInteger)taskIdentifier withCall:(HCCallHandle)call;
@end
Loading