From b7a2ecd863abefb26d8ba288bda503f10d9a34b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20K=C3=A4fer?= Date: Fri, 25 Jan 2019 13:51:29 +0100 Subject: [PATCH] perform XHR in the main context rather than the worker's to preserve referrer information IE11 doesn't send the correct referrer for XHRs in a web worker. Instead, we're now doing the request from the main context and transfering the result back to the worker. Most modern browsers will continue to use the Fetch API and will not be affected by this change. --- src/style/style.js | 7 ++++++- src/util/actor.js | 25 ++++++++++++++++++++++--- src/util/ajax.js | 26 ++++++++++++++++++++++---- test/ajax_stubs.js | 2 ++ 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/style/style.js b/src/style/style.js index a9c07ce169f..5673f87a3e5 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -11,7 +11,7 @@ import GlyphManager from '../render/glyph_manager'; import Light from './light'; import LineAtlas from '../render/line_atlas'; import { pick, clone, extend, deepEqual, filterObject, mapObject } from '../util/util'; -import { getJSON, getReferrer, ResourceType } from '../util/ajax'; +import { getJSON, getReferrer, makeRequest, ResourceType } from '../util/ajax'; import { isMapboxURL, normalizeStyleURL } from '../util/mapbox'; import browser from '../util/browser'; import Dispatcher from '../util/dispatcher'; @@ -51,6 +51,7 @@ import type {Callback} from '../types/callback'; import type EvaluationParameters from './evaluation_parameters'; import type {Placement} from '../symbol/placement'; import type {Cancelable} from '../types/cancelable'; +import type {RequestParameters, ResponseCallback} from '../util/ajax'; import type {GeoJSON} from '@mapbox/geojson-types'; import type { LayerSpecification, @@ -1213,6 +1214,10 @@ class Style extends Evented { getGlyphs(mapId: string, params: {stacks: {[string]: Array}}, callback: Callback<{[string]: {[number]: ?StyleGlyph}}>) { this.glyphManager.getGlyphs(params.stacks, callback); } + + getResource(mapId: string, params: RequestParameters, callback: ResponseCallback): Cancelable { + return makeRequest(params, callback); + } } Style.getSourceType = getSourceType; diff --git a/src/util/actor.js b/src/util/actor.js index 357a4e8aa28..e663f5e261a 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -4,6 +4,7 @@ import { bindAll } from './util'; import { serialize, deserialize } from './web_worker_transfer'; import type {Transferable} from '../types/transferable'; +import type {Cancelable} from '../types/cancelable'; /** * An implementation of the [Actor design pattern](http://en.wikipedia.org/wiki/Actor_model) @@ -42,7 +43,7 @@ class Actor { * @param targetMapId A particular mapId to which to send this message. * @private */ - send(type: string, data: mixed, callback: ?Function, targetMapId: ?string) { + send(type: string, data: mixed, callback: ?Function, targetMapId: ?string): ?Cancelable { const id = callback ? `${this.mapId}:${this.callbackID++}` : null; if (callback) this.callbacks[id] = callback; const buffers: Array = []; @@ -53,6 +54,16 @@ class Actor { id: String(id), data: serialize(data, buffers) }, buffers); + if (callback) { + return { + cancel: () => this.target.postMessage({ + targetMapId, + sourceMapId: this.mapId, + type: '', + id: String(id) + }) + }; + } } receive(message: Object) { @@ -64,6 +75,7 @@ class Actor { return; const done = (err, data) => { + delete this.callbacks[id]; const buffers: Array = []; this.target.postMessage({ sourceMapId: this.mapId, @@ -74,7 +86,7 @@ class Actor { }, buffers); }; - if (data.type === '') { + if (data.type === '' || data.type === '') { callback = this.callbacks[data.id]; delete this.callbacks[data.id]; if (callback && data.error) { @@ -84,7 +96,14 @@ class Actor { } } else if (typeof data.id !== 'undefined' && this.parent[data.type]) { // data.type == 'loadTile', 'removeTile', etc. - this.parent[data.type](data.sourceMapId, deserialize(data.data), done); + // Add a placeholder so that we can discover when the done callback was called already. + this.callbacks[data.id] = null; + const cancelable = this.parent[data.type](data.sourceMapId, deserialize(data.data), done); + if (cancelable && this.callbacks[data.id] === null) { + // Only add the cancelable callback if the done callback wasn't already called. + // Otherwise we will never be able to delete it. + this.callbacks[data.id] = cancelable; + } } else if (typeof data.id !== 'undefined' && this.parent.getWorkerSource) { // data.type == sourcetype.method const keys = data.type.split('.'); diff --git a/src/util/ajax.js b/src/util/ajax.js index a9cf1b5b2cb..dff48a7294c 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -71,14 +71,17 @@ class AJAXError extends Error { } } +function isWorker() { + return typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && + self instanceof WorkerGlobalScope; +} + // Ensure that we're sending the correct referrer from blob URL worker bundles. // For files loaded from the local file system, `location.origin` will be set // to the string(!) "null" (Firefox), or "file://" (Chrome, Safari, Edge, IE), // and we will set an empty referrer. Otherwise, we're using the document's URL. /* global self, WorkerGlobalScope */ -export const getReferrer = typeof WorkerGlobalScope !== 'undefined' && - typeof self !== 'undefined' && - self instanceof WorkerGlobalScope ? +export const getReferrer = isWorker() ? () => self.worker && self.worker.referrer : () => { const origin = window.location.origin; @@ -158,7 +161,22 @@ function makeXMLHttpRequest(requestParameters: RequestParameters, callback: Resp return { cancel: () => xhr.abort() }; } -const makeRequest = window.fetch && window.Request && window.AbortController ? makeFetchRequest : makeXMLHttpRequest; +export const makeRequest = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { + // We're trying to use the Fetch API if possible. However, in some situations we can't use it: + // - IE11 doesn't support it at all. In this case, we dispatch the request to the main thread so + // that we can get an accruate referrer header. + // - Requests for resources with the file:// URI scheme don't work with the Fetch API either. In + // this case we unconditionally use XHR on the current thread since referrers don't matter. + if (!/^file:/.test(requestParameters.url)) { + if (window.fetch && window.Request && window.AbortController) { + return makeFetchRequest(requestParameters, callback); + } + if (isWorker() && self.worker && self.worker.actor) { + return self.worker.actor.send('getResource', requestParameters, callback); + } + } + return makeXMLHttpRequest(requestParameters, callback); +}; export const getJSON = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { return makeRequest(extend(requestParameters, { type: 'json' }), callback); diff --git a/test/ajax_stubs.js b/test/ajax_stubs.js index b45f684808b..70d61fa1169 100644 --- a/test/ajax_stubs.js +++ b/test/ajax_stubs.js @@ -67,6 +67,8 @@ export const getArrayBuffer = function({ url }, callback) { }); }; +export const makeRequest = getArrayBuffer; + export const postData = function({ url, body }, callback) { return request.post(url, body, (error, response, body) => { if (!error && response.statusCode >= 200 && response.statusCode < 300) {