434 lines
14 KiB
Plaintext
434 lines
14 KiB
Plaintext
#include <mbgl/storage/default/http_request.hpp>
|
|
#include <mbgl/storage/default/http_context.hpp>
|
|
#include <mbgl/storage/response.hpp>
|
|
#include <mbgl/util/uv.hpp>
|
|
|
|
#include <mbgl/util/time.hpp>
|
|
#include <mbgl/util/parsedate.h>
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#include <map>
|
|
#include <cassert>
|
|
|
|
namespace mbgl {
|
|
|
|
enum class ResponseStatus : uint8_t {
|
|
// This error probably won't be resolved by retrying anytime soon. We are giving up.
|
|
PermanentError,
|
|
|
|
// This error might be resolved by waiting some time (e.g. server issues).
|
|
// We are going to do an exponential back-off and will try again in a few seconds.
|
|
TemporaryError,
|
|
|
|
// This error was caused by a temporary error and it is likely that it will be resolved
|
|
// immediately. We are going to try again right away. This is like the TemporaryError, except
|
|
// that we will not perform exponential back-off.
|
|
SingularError,
|
|
|
|
// This error might be resolved once the network reachability status changes.
|
|
// We are going to watch the network status for changes and will retry as soon as the
|
|
// operating system notifies us of a network status change.
|
|
ConnectionError,
|
|
|
|
// The request was canceled mid-way.
|
|
Canceled,
|
|
|
|
// The request returned data successfully. We retrieved and decoded the data successfully.
|
|
Successful,
|
|
|
|
// The request confirmed that the data wasn't changed. We already have the data.
|
|
NotModified,
|
|
};
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
class HTTPNSURLContext;
|
|
|
|
class HTTPRequestImpl {
|
|
public:
|
|
HTTPRequestImpl(HTTPRequest *request, uv_loop_t *loop, std::unique_ptr<Response> response);
|
|
~HTTPRequestImpl();
|
|
|
|
void cancel();
|
|
void cancelTimer();
|
|
|
|
void start();
|
|
void handleResult(NSData *data, NSURLResponse *res, NSError *error);
|
|
void handleResponse();
|
|
|
|
void retry(uint64_t timeout);
|
|
void retryImmediately();
|
|
static void restart(uv_timer_t *timer, int);
|
|
|
|
private:
|
|
HTTPNSURLContext *context = nullptr;
|
|
HTTPRequest *request = nullptr;
|
|
NSURLSessionDataTask *task = nullptr;
|
|
std::unique_ptr<Response> response;
|
|
std::unique_ptr<Response> existingResponse;
|
|
ResponseStatus status = ResponseStatus::PermanentError;
|
|
uv_async_t *async = nullptr;
|
|
int attempts = 0;
|
|
uv_timer_t *timer = nullptr;
|
|
enum : bool { PreemptImmediately, ExponentialBackoff } strategy = PreemptImmediately;
|
|
|
|
static const int maxAttempts = 4;
|
|
};
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
class HTTPNSURLContext : public HTTPContext<HTTPNSURLContext> {
|
|
public:
|
|
HTTPNSURLContext(uv_loop_t *loop);
|
|
~HTTPNSURLContext();
|
|
|
|
NSURLSession *session = nil;
|
|
NSString *userAgent = nil;
|
|
};
|
|
|
|
template<> pthread_key_t ThreadContext<HTTPNSURLContext>::key{};
|
|
template<> pthread_once_t ThreadContext<HTTPNSURLContext>::once = PTHREAD_ONCE_INIT;
|
|
|
|
HTTPNSURLContext::HTTPNSURLContext(uv_loop_t *loop_) : HTTPContext(loop_) {
|
|
@autoreleasepool {
|
|
NSURLSessionConfiguration *sessionConfig =
|
|
[NSURLSessionConfiguration defaultSessionConfiguration];
|
|
sessionConfig.timeoutIntervalForResource = 30;
|
|
sessionConfig.HTTPMaximumConnectionsPerHost = 8;
|
|
sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
|
sessionConfig.URLCache = nil;
|
|
|
|
session = [NSURLSession sessionWithConfiguration:sessionConfig];
|
|
[session retain];
|
|
|
|
// Write user agent string
|
|
userAgent = @"MapboxGL";
|
|
}
|
|
}
|
|
|
|
HTTPNSURLContext::~HTTPNSURLContext() {
|
|
[session release];
|
|
session = nullptr;
|
|
|
|
[userAgent release];
|
|
userAgent = nullptr;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
HTTPRequestImpl::HTTPRequestImpl(HTTPRequest *request_, uv_loop_t *loop,
|
|
std::unique_ptr<Response> existingResponse_)
|
|
: context(HTTPNSURLContext::Get(loop)),
|
|
request(request_),
|
|
existingResponse(std::move(existingResponse_)),
|
|
async(new uv_async_t) {
|
|
assert(request);
|
|
context->addRequest(request);
|
|
|
|
async->data = this;
|
|
uv_async_init(loop, async, [](uv_async_t *as, int) {
|
|
auto impl = reinterpret_cast<HTTPRequestImpl *>(as->data);
|
|
impl->handleResponse();
|
|
});
|
|
|
|
start();
|
|
}
|
|
|
|
void HTTPRequestImpl::start() {
|
|
assert(!task);
|
|
|
|
attempts++;
|
|
|
|
@autoreleasepool {
|
|
|
|
NSMutableString *url = [[NSMutableString alloc] initWithString:@(request->resource.url.c_str())];
|
|
if ([[NSUserDefaults standardUserDefaults] objectForKey:@"mapbox_metrics_disabled"] == nil) {
|
|
if ([url rangeOfString:@"?"].location == NSNotFound) {
|
|
[url appendString:@"?"];
|
|
} else {
|
|
[url appendString:@"&"];
|
|
}
|
|
[url appendString:@"events=true"];
|
|
}
|
|
|
|
NSMutableURLRequest *req = [[NSMutableURLRequest alloc]
|
|
initWithURL:[NSURL URLWithString:url]];
|
|
if (existingResponse) {
|
|
if (!existingResponse->etag.empty()) {
|
|
[req addValue:@(existingResponse->etag.c_str()) forHTTPHeaderField:@"If-None-Match"];
|
|
} else if (existingResponse->modified) {
|
|
const std::string time = util::rfc1123(existingResponse->modified);
|
|
[req addValue:@(time.c_str()) forHTTPHeaderField:@"If-Modified-Since"];
|
|
}
|
|
}
|
|
|
|
[req addValue:context->userAgent forHTTPHeaderField:@"User-Agent"];
|
|
|
|
task = [context->session dataTaskWithRequest:req
|
|
completionHandler:^(NSData *data, NSURLResponse *res,
|
|
NSError *error) { handleResult(data, res, error); }];
|
|
[req release];
|
|
[task retain];
|
|
[task resume];
|
|
}
|
|
}
|
|
|
|
void HTTPRequestImpl::handleResponse() {
|
|
if (task) {
|
|
[task release];
|
|
task = nullptr;
|
|
}
|
|
|
|
if (request) {
|
|
if (status == ResponseStatus::TemporaryError && attempts < maxAttempts) {
|
|
strategy = ExponentialBackoff;
|
|
return retry((1 << (attempts - 1)) * 1000);
|
|
} else if (status == ResponseStatus::ConnectionError && attempts < maxAttempts) {
|
|
// By default, we will retry every 30 seconds (network change notification will
|
|
// preempt the timeout).
|
|
strategy = PreemptImmediately;
|
|
return retry(30000);
|
|
}
|
|
|
|
// Actually return the response.
|
|
if (status == ResponseStatus::NotModified) {
|
|
request->notify(std::move(response), FileCache::Hint::Refresh);
|
|
} else {
|
|
request->notify(std::move(response), FileCache::Hint::Full);
|
|
}
|
|
|
|
context->removeRequest(request);
|
|
request->ptr = nullptr;
|
|
delete request;
|
|
request = nullptr;
|
|
}
|
|
|
|
delete this;
|
|
}
|
|
|
|
void HTTPRequestImpl::cancel() {
|
|
context->removeRequest(request);
|
|
request = nullptr;
|
|
|
|
// Stop the backoff timer to avoid re-triggering this request.
|
|
cancelTimer();
|
|
|
|
if (task) {
|
|
[task cancel];
|
|
[task release];
|
|
task = nullptr;
|
|
} else {
|
|
delete this;
|
|
}
|
|
}
|
|
|
|
void HTTPRequestImpl::cancelTimer() {
|
|
if (timer) {
|
|
uv_timer_stop(timer);
|
|
uv::close(timer);
|
|
timer = nullptr;
|
|
}
|
|
}
|
|
|
|
HTTPRequestImpl::~HTTPRequestImpl() {
|
|
assert(!task);
|
|
assert(async);
|
|
|
|
// Stop the backoff timer to avoid re-triggering this request.
|
|
cancelTimer();
|
|
|
|
uv::close(async);
|
|
|
|
if (request) {
|
|
context->removeRequest(request);
|
|
request->ptr = nullptr;
|
|
}
|
|
}
|
|
|
|
int64_t parseCacheControl(const char *value) {
|
|
if (value) {
|
|
unsigned long long seconds = 0;
|
|
// TODO: cache-control may contain other information as well:
|
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
|
|
if (std::sscanf(value, "max-age=%llu", &seconds) == 1) {
|
|
return std::chrono::duration_cast<std::chrono::seconds>(
|
|
std::chrono::system_clock::now().time_since_epoch()).count() +
|
|
seconds;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void HTTPRequestImpl::handleResult(NSData *data, NSURLResponse *res, NSError *error) {
|
|
if (error) {
|
|
if ([error code] == NSURLErrorCancelled) {
|
|
status = ResponseStatus::Canceled;
|
|
} else {
|
|
// TODO: Use different codes for host not found, timeout, invalid URL etc.
|
|
// These can be categorized in temporary and permanent errors.
|
|
response = util::make_unique<Response>();
|
|
response->status = Response::Error;
|
|
response->message = [[error localizedDescription] UTF8String];
|
|
|
|
switch ([error code]) {
|
|
case NSURLErrorBadServerResponse: // 5xx errors
|
|
status = ResponseStatus::TemporaryError;
|
|
break;
|
|
|
|
case NSURLErrorTimedOut:
|
|
case NSURLErrorUserCancelledAuthentication:
|
|
status = ResponseStatus::SingularError; // retry immediately
|
|
break;
|
|
|
|
case NSURLErrorNetworkConnectionLost:
|
|
case NSURLErrorCannotFindHost:
|
|
case NSURLErrorCannotConnectToHost:
|
|
case NSURLErrorDNSLookupFailed:
|
|
case NSURLErrorNotConnectedToInternet:
|
|
case NSURLErrorInternationalRoamingOff:
|
|
case NSURLErrorCallIsActive:
|
|
case NSURLErrorDataNotAllowed:
|
|
status = ResponseStatus::ConnectionError;
|
|
break;
|
|
|
|
default:
|
|
status = ResponseStatus::PermanentError;
|
|
}
|
|
}
|
|
} else if ([res isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
const long responseCode = [(NSHTTPURLResponse *)res statusCode];
|
|
|
|
response = util::make_unique<Response>();
|
|
response->data = {(const char *)[data bytes], [data length]};
|
|
|
|
NSDictionary *headers = [(NSHTTPURLResponse *)res allHeaderFields];
|
|
NSString *cache_control = [headers objectForKey:@"Cache-Control"];
|
|
if (cache_control) {
|
|
response->expires = parseCacheControl([cache_control UTF8String]);
|
|
}
|
|
|
|
NSString *expires = [headers objectForKey:@"Expires"];
|
|
if (expires) {
|
|
response->expires = parse_date([expires UTF8String]);
|
|
}
|
|
|
|
NSString *last_modified = [headers objectForKey:@"Last-Modified"];
|
|
if (last_modified) {
|
|
response->modified = parse_date([last_modified UTF8String]);
|
|
}
|
|
|
|
NSString *etag = [headers objectForKey:@"ETag"];
|
|
if (etag) {
|
|
response->etag = [etag UTF8String];
|
|
}
|
|
|
|
if (responseCode == 304) {
|
|
if (existingResponse) {
|
|
// We're going to reuse the old response object, but need to copy over the new
|
|
// expires value (if possible).
|
|
std::swap(response, existingResponse);
|
|
if (existingResponse->expires) {
|
|
response->expires = existingResponse->expires;
|
|
}
|
|
status = ResponseStatus::NotModified;
|
|
} else {
|
|
// This is an unsolicited 304 response and should only happen on malfunctioning
|
|
// HTTP servers. It likely doesn't include any data, but we don't have much options.
|
|
response->status = Response::Successful;
|
|
status = ResponseStatus::Successful;
|
|
}
|
|
} else if (responseCode == 200) {
|
|
response->status = Response::Successful;
|
|
status = ResponseStatus::Successful;
|
|
} else if (responseCode >= 500 && responseCode < 600) {
|
|
// Server errors may be temporary, so back off exponentially.
|
|
response->status = Response::Error;
|
|
response->message = "HTTP status code " + std::to_string(responseCode);
|
|
status = ResponseStatus::TemporaryError;
|
|
} else {
|
|
// We don't know how to handle any other errors, so declare them as permanently failing.
|
|
response->status = Response::Error;
|
|
response->message = "HTTP status code " + std::to_string(responseCode);
|
|
status = ResponseStatus::PermanentError;
|
|
}
|
|
} else {
|
|
// This should never happen.
|
|
status = ResponseStatus::PermanentError;
|
|
response = util::make_unique<Response>();
|
|
response->status = Response::Error;
|
|
response->message = "response class is not NSHTTPURLResponse";
|
|
}
|
|
|
|
uv_async_send(async);
|
|
}
|
|
|
|
void HTTPRequestImpl::retry(uint64_t timeout) {
|
|
response.reset();
|
|
|
|
assert(!timer);
|
|
timer = new uv_timer_t;
|
|
timer->data = this;
|
|
uv_timer_init(async->loop, timer);
|
|
uv_timer_start(timer, restart, timeout, 0);
|
|
}
|
|
|
|
void HTTPRequestImpl::retryImmediately() {
|
|
// All batons get notified when the network status changed, but some of them
|
|
// might not actually wait for the network to become available again.
|
|
if (timer && strategy == PreemptImmediately) {
|
|
// Triggers the timer upon the next event loop iteration.
|
|
uv_timer_stop(timer);
|
|
uv_timer_start(timer, restart, 0, 0);
|
|
}
|
|
}
|
|
|
|
void HTTPRequestImpl::restart(uv_timer_t *timer, int) {
|
|
// Restart the request.
|
|
auto impl = reinterpret_cast<HTTPRequestImpl *>(timer->data);
|
|
|
|
// Get rid of the timer.
|
|
impl->timer = nullptr;
|
|
uv::close(timer);
|
|
|
|
impl->start();
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
HTTPRequest::HTTPRequest(DefaultFileSource *source, const Resource &resource)
|
|
: SharedRequestBase(source, resource) {
|
|
}
|
|
|
|
HTTPRequest::~HTTPRequest() {
|
|
MBGL_VERIFY_THREAD(tid);
|
|
|
|
if (ptr) {
|
|
reinterpret_cast<HTTPRequestImpl *>(ptr)->cancel();
|
|
}
|
|
}
|
|
|
|
void HTTPRequest::start(uv_loop_t *loop, std::unique_ptr<Response> response) {
|
|
MBGL_VERIFY_THREAD(tid);
|
|
|
|
assert(!ptr);
|
|
ptr = new HTTPRequestImpl(this, loop, std::move(response));
|
|
}
|
|
|
|
void HTTPRequest::retryImmediately() {
|
|
MBGL_VERIFY_THREAD(tid);
|
|
|
|
if (ptr) {
|
|
reinterpret_cast<HTTPRequestImpl *>(ptr)->retryImmediately();
|
|
}
|
|
}
|
|
|
|
void HTTPRequest::cancel() {
|
|
MBGL_VERIFY_THREAD(tid);
|
|
|
|
delete this;
|
|
}
|
|
|
|
}
|